diff --git a/pom.xml b/pom.xml index e6b8d527d..5a6b85ac4 100644 --- a/pom.xml +++ b/pom.xml @@ -158,11 +158,18 @@ nimbus-jose-jwt ${nimbus-jose.version} + + com.fasterxml.jackson.core jackson-databind ${jackson.version} + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 4d956c180..368c4f8c2 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -38,6 +38,7 @@ open module org.cryptomator.desktop { requires com.auth0.jwt; requires com.google.common; requires com.fasterxml.jackson.databind; + requires com.fasterxml.jackson.datatype.jsr310; requires com.nimbusds.jose.jwt; requires com.nulabinc.zxcvbn; requires com.tobiasdiez.easybind; diff --git a/src/main/java/org/cryptomator/common/settings/Settings.java b/src/main/java/org/cryptomator/common/settings/Settings.java index 4e0e0df97..a54e71a4f 100644 --- a/src/main/java/org/cryptomator/common/settings/Settings.java +++ b/src/main/java/org/cryptomator/common/settings/Settings.java @@ -25,6 +25,7 @@ import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.geometry.NodeOrientation; +import java.time.Instant; import java.util.function.Consumer; public class Settings { @@ -44,8 +45,7 @@ public class Settings { static final String DEFAULT_KEYCHAIN_PROVIDER = SystemUtils.IS_OS_WINDOWS ? "org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess" : SystemUtils.IS_OS_MAC ? "org.cryptomator.macos.keychain.MacSystemKeychainAccess" : "org.cryptomator.linux.keychain.SecretServiceKeychainAccess"; static final String DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT.name(); static final boolean DEFAULT_SHOW_MINIMIZE_BUTTON = false; - static final String DEFAULT_LAST_UPDATE_CHECK = "2000-01-01"; - + public static final Instant DEFAULT_TIMESTAMP = Instant.parse("2000-01-01T00:00:00Z"); public final ObservableList directories; public final BooleanProperty askedForUpdateCheck; public final BooleanProperty checkForUpdates; @@ -67,7 +67,7 @@ public class Settings { public final IntegerProperty windowHeight; public final StringProperty language; public final StringProperty mountService; - public final StringProperty lastUpdateCheck; + public final ObjectProperty lastSuccessfulUpdateCheck; private Consumer saveCmd; @@ -104,7 +104,7 @@ public class Settings { this.windowHeight = new SimpleIntegerProperty(this, "windowHeight", json.windowHeight); this.language = new SimpleStringProperty(this, "language", json.language); this.mountService = new SimpleStringProperty(this, "mountService", json.mountService); - this.lastUpdateCheck = new SimpleStringProperty(this, "lastUpdateCheck", json.lastUpdateCheck); + this.lastSuccessfulUpdateCheck = new SimpleObjectProperty<>(this, "lastSuccessfulUpdateCheck", json.lastSuccessfulUpdateCheck); this.directories.addAll(json.directories.stream().map(VaultSettings::new).toList()); @@ -131,7 +131,7 @@ public class Settings { windowHeight.addListener(this::somethingChanged); language.addListener(this::somethingChanged); mountService.addListener(this::somethingChanged); - lastUpdateCheck.addListener(this::somethingChanged); + lastSuccessfulUpdateCheck.addListener(this::somethingChanged); } @SuppressWarnings("deprecation") @@ -185,7 +185,7 @@ public class Settings { json.windowHeight = windowHeight.get(); json.language = language.get(); json.mountService = mountService.get(); - json.lastUpdateCheck = lastUpdateCheck.get(); + json.lastSuccessfulUpdateCheck = lastSuccessfulUpdateCheck.get(); return json; } diff --git a/src/main/java/org/cryptomator/common/settings/SettingsJson.java b/src/main/java/org/cryptomator/common/settings/SettingsJson.java index 2c7c963da..2ded82885 100644 --- a/src/main/java/org/cryptomator/common/settings/SettingsJson.java +++ b/src/main/java/org/cryptomator/common/settings/SettingsJson.java @@ -1,9 +1,11 @@ package org.cryptomator.common.settings; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Instant; import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) @@ -80,7 +82,8 @@ class SettingsJson { @JsonProperty(value = "preferredVolumeImpl", access = JsonProperty.Access.WRITE_ONLY) // WRITE_ONLY means value is "written" into the java object during deserialization. Upvote this: https://github.com/FasterXML/jackson-annotations/issues/233 String preferredVolumeImpl; - @JsonProperty("lastUpdateCheck") - String lastUpdateCheck = Settings.DEFAULT_LAST_UPDATE_CHECK; + @JsonProperty("lastSuccessfulUpdateCheck") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", timezone = "UTC") + Instant lastSuccessfulUpdateCheck = Settings.DEFAULT_TIMESTAMP; } diff --git a/src/main/java/org/cryptomator/common/settings/SettingsProvider.java b/src/main/java/org/cryptomator/common/settings/SettingsProvider.java index 586708ed1..a33b51027 100644 --- a/src/main/java/org/cryptomator/common/settings/SettingsProvider.java +++ b/src/main/java/org/cryptomator/common/settings/SettingsProvider.java @@ -10,6 +10,7 @@ package org.cryptomator.common.settings; import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.google.common.base.Suppliers; import org.cryptomator.common.Environment; import org.slf4j.Logger; @@ -36,7 +37,7 @@ import java.util.stream.Stream; @Singleton public class SettingsProvider implements Supplier { - private static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true); + private static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true).registerModule(new JavaTimeModule()); private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class); private static final long SAVE_DELAY_MS = 1000; diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java index 709eb2fe7..b857adcae 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java +++ b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java @@ -1,45 +1,57 @@ package org.cryptomator.ui.fxapp; import org.cryptomator.common.Environment; +import org.cryptomator.common.SemVerComparator; import org.cryptomator.common.settings.Settings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javax.inject.Named; +import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.concurrent.ScheduledService; import javafx.concurrent.Worker; import javafx.concurrent.WorkerStateEvent; import javafx.util.Duration; +import java.time.Instant; import java.util.Comparator; @FxApplicationScoped public class UpdateChecker { private static final Logger LOG = LoggerFactory.getLogger(UpdateChecker.class); - private static final Duration AUTOCHECK_DELAY = Duration.seconds(5); + private static final Duration AUTO_CHECK_DELAY = Duration.seconds(5); private final Environment env; private final Settings settings; - private final StringProperty latestVersionProperty; - private final Comparator semVerComparator; + private final StringProperty latestVersion = new SimpleStringProperty(); private final ScheduledService updateCheckerService; + private final ObjectProperty state = new SimpleObjectProperty<>(UpdateCheckState.NOT_CHECKED); + private final ObjectProperty lastSuccessfulUpdateCheck; + private final Comparator versionComparator = new SemVerComparator(); + private final BooleanBinding updateAvailable; + private final BooleanBinding checkFailed; @Inject - UpdateChecker(Settings settings, Environment env, @Named("latestVersion") StringProperty latestVersionProperty, @Named("SemVer") Comparator semVerComparator, ScheduledService updateCheckerService) { + UpdateChecker(Settings settings, // + Environment env, // + ScheduledService updateCheckerService) { this.env = env; this.settings = settings; - this.latestVersionProperty = latestVersionProperty; - this.semVerComparator = semVerComparator; this.updateCheckerService = updateCheckerService; + this.lastSuccessfulUpdateCheck = settings.lastSuccessfulUpdateCheck; + this.updateAvailable = Bindings.createBooleanBinding(this::isUpdateAvailable, latestVersion); + this.checkFailed = Bindings.equal(UpdateCheckState.CHECK_FAILED, state); } public void automaticallyCheckForUpdatesIfEnabled() { if (!env.disableUpdateCheck() && settings.checkForUpdates.get()) { - startCheckingForUpdates(AUTOCHECK_DELAY); + startCheckingForUpdates(AUTO_CHECK_DELAY); } } @@ -59,36 +71,65 @@ public class UpdateChecker { private void checkStarted(WorkerStateEvent event) { LOG.debug("Checking for updates..."); + state.set(UpdateCheckState.IS_CHECKING); } private void checkSucceeded(WorkerStateEvent event) { - String latestVersion = updateCheckerService.getValue(); - LOG.info("Current version: {}, lastest version: {}", getCurrentVersion(), latestVersion); - - if (semVerComparator.compare(getCurrentVersion(), latestVersion) < 0) { - // update is available - latestVersionProperty.set(latestVersion); - } else { - latestVersionProperty.set(null); - } + var latestVersionString = updateCheckerService.getValue(); + LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), latestVersionString); + lastSuccessfulUpdateCheck.set(Instant.now()); + latestVersion.set(latestVersionString); + state.set(UpdateCheckState.CHECK_SUCCESSFUL); } private void checkFailed(WorkerStateEvent event) { - LOG.warn("Error checking for updates", event.getSource().getException()); + state.set(UpdateCheckState.CHECK_FAILED); + } + + public enum UpdateCheckState { + NOT_CHECKED, + IS_CHECKING, + CHECK_SUCCESSFUL, + CHECK_FAILED; } /* Observable Properties */ - public BooleanBinding checkingForUpdatesProperty() { return updateCheckerService.stateProperty().isEqualTo(Worker.State.RUNNING); } public ReadOnlyStringProperty latestVersionProperty() { - return latestVersionProperty; + return latestVersion; + } + + public BooleanBinding updateAvailableProperty() { + return updateAvailable; + } + + public BooleanBinding checkFailedProperty() { + return checkFailed; + } + + public boolean isUpdateAvailable() { + String currentVersion = getCurrentVersion(); + String latestVersionString = latestVersion.get(); + + if (currentVersion == null || latestVersionString == null) { + return false; + } else { + return versionComparator.compare(currentVersion, latestVersionString) < 0; + } + } + + public ObjectProperty lastSuccessfulUpdateCheckProperty() { + return lastSuccessfulUpdateCheck; + } + + public ObjectProperty updateCheckStateProperty() { + return state; } public String getCurrentVersion() { return env.getAppVersion(); } - } diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java index b5f06d7e5..585180662 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java +++ b/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java @@ -11,8 +11,6 @@ import org.slf4j.LoggerFactory; import javax.inject.Named; import javafx.beans.binding.Bindings; import javafx.beans.binding.ObjectBinding; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; import javafx.util.Duration; @@ -32,13 +30,6 @@ public abstract class UpdateCheckerModule { private static final Duration UPDATE_CHECK_INTERVAL = Duration.hours(3); private static final Duration DISABLED_UPDATE_CHECK_INTERVAL = Duration.hours(100000); // Duration.INDEFINITE leads to overflows... - @Provides - @Named("latestVersion") - @FxApplicationScoped - static StringProperty provideLatestVersion() { - return new SimpleStringProperty(); - } - @Provides @FxApplicationScoped static Optional provideHttpClient() { diff --git a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java index 479a2d860..f3c92790d 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java @@ -46,7 +46,7 @@ public class MainWindowTitleController implements FxController { this.appWindows = appWindows; this.trayMenuInitialized = trayMenu.isInitialized(); this.updateChecker = updateChecker; - this.updateAvailable = updateChecker.latestVersionProperty().isNotNull(); + this.updateAvailable = updateChecker.updateAvailableProperty(); this.licenseHolder = licenseHolder; this.settings = settings; this.showMinimizeButton = Bindings.createBooleanBinding(this::isShowMinimizeButton, settings.showMinimizeButton, settings.showTrayIcon); diff --git a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java index 630b82776..afa05cc8c 100644 --- a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java @@ -1,18 +1,33 @@ package org.cryptomator.ui.preferences; +import org.cryptomator.common.Environment; import org.cryptomator.common.settings.Settings; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.fxapp.UpdateChecker; import javax.inject.Inject; +import javafx.animation.PauseTransition; import javafx.application.Application; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.ObjectBinding; +import javafx.beans.binding.StringBinding; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; import javafx.scene.control.CheckBox; import javafx.scene.control.ContentDisplay; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.Locale; +import java.util.ResourceBundle; + @PreferencesScoped public class UpdatesPreferencesController implements FxController { @@ -20,29 +35,55 @@ public class UpdatesPreferencesController implements FxController { private static final String DOWNLOADS_URI = "https://cryptomator.org/downloads"; private final Application application; + private final Environment environment; + private final ResourceBundle resourceBundle; private final Settings settings; private final UpdateChecker updateChecker; private final ObjectBinding checkForUpdatesButtonState; private final ReadOnlyStringProperty latestVersion; + private final ObservableValue lastSuccessfulUpdateCheck; + private final StringBinding lastUpdateCheckMessage; + private final ObservableValue timeDifferenceMessage; private final String currentVersion; private final BooleanBinding updateAvailable; + private final BooleanBinding checkFailed; + private final BooleanProperty upToDateLabelVisible = new SimpleBooleanProperty(false); + private final DateTimeFormatter formatter; + private final BooleanBinding upToDate; /* FXML */ public CheckBox checkForUpdatesCheckbox; @Inject - UpdatesPreferencesController(Application application, Settings settings, UpdateChecker updateChecker) { + UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker) { this.application = application; + this.environment = environment; + this.resourceBundle = resourceBundle; this.settings = settings; this.updateChecker = updateChecker; this.checkForUpdatesButtonState = Bindings.when(updateChecker.checkingForUpdatesProperty()).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY); this.latestVersion = updateChecker.latestVersionProperty(); - this.updateAvailable = latestVersion.isNotNull(); + this.lastSuccessfulUpdateCheck = updateChecker.lastSuccessfulUpdateCheckProperty(); + this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, lastSuccessfulUpdateCheck); this.currentVersion = updateChecker.getCurrentVersion(); + this.updateAvailable = updateChecker.updateAvailableProperty(); + this.formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault()); + this.upToDate = updateChecker.updateCheckStateProperty().isEqualTo(UpdateChecker.UpdateCheckState.CHECK_SUCCESSFUL).and(latestVersion.isEqualTo(currentVersion)); + this.checkFailed = updateChecker.checkFailedProperty(); + this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, lastSuccessfulUpdateCheck); } public void initialize() { checkForUpdatesCheckbox.selectedProperty().bindBidirectional(settings.checkForUpdates); + + upToDate.addListener((_, _, newVal) -> { + if (newVal) { + upToDateLabelVisible.set(true); + PauseTransition delay = new PauseTransition(javafx.util.Duration.seconds(5)); + delay.setOnFinished(_ -> upToDateLabelVisible.set(false)); + delay.play(); + } + }); } @FXML @@ -55,6 +96,11 @@ public class UpdatesPreferencesController implements FxController { application.getHostServices().showDocument(DOWNLOADS_URI); } + @FXML + public void showLogfileDirectory() { + environment.getLogDir().ifPresent(logDirPath -> application.getHostServices().showDocument(logDirPath.toUri().toString())); + } + /* Observable Properties */ public ObjectBinding checkForUpdatesButtonStateProperty() { @@ -77,6 +123,46 @@ public class UpdatesPreferencesController implements FxController { return currentVersion; } + public StringBinding lastUpdateCheckMessageProperty() { + return lastUpdateCheckMessage; + } + + public String getLastUpdateCheckMessage() { + Instant lastCheck = lastSuccessfulUpdateCheck.getValue(); + if (lastCheck != null && !lastCheck.equals(Settings.DEFAULT_TIMESTAMP)) { + return formatter.format(LocalDateTime.ofInstant(lastCheck, ZoneId.systemDefault())); + } else { + return "-"; + } + } + + public ObservableValue timeDifferenceMessageProperty() { + return timeDifferenceMessage; + } + + public String getTimeDifferenceMessage() { + var lastSuccessCheck = lastSuccessfulUpdateCheck.getValue(); + var duration = Duration.between(lastSuccessCheck, Instant.now()); + var hours = duration.toHours(); + if (lastSuccessCheck.equals(Settings.DEFAULT_TIMESTAMP)) { + return resourceBundle.getString("preferences.updates.lastUpdateCheck.never"); + } else if (hours < 1) { + return resourceBundle.getString("preferences.updates.lastUpdateCheck.recently"); + } else if (hours < 24) { + return String.format(resourceBundle.getString("preferences.updates.lastUpdateCheck.hoursAgo"), hours); + } else { + return String.format(resourceBundle.getString("preferences.updates.lastUpdateCheck.daysAgo"), duration.toDays()); + } + } + + public BooleanProperty upToDateLabelVisibleProperty() { + return upToDateLabelVisible; + } + + public boolean isUpToDateLabelVisible() { + return upToDateLabelVisible.get(); + } + public BooleanBinding updateAvailableProperty() { return updateAvailable; } @@ -84,4 +170,13 @@ public class UpdatesPreferencesController implements FxController { public boolean isUpdateAvailable() { return updateAvailable.get(); } + + public BooleanBinding checkFailedProperty() { + return checkFailed; + } + + public boolean isCheckFailed() { + return checkFailed.getValue(); + } + } diff --git a/src/main/java/org/cryptomator/ui/updatereminder/UpdateReminderComponent.java b/src/main/java/org/cryptomator/ui/updatereminder/UpdateReminderComponent.java index aa13f30da..d2c10f8fd 100644 --- a/src/main/java/org/cryptomator/ui/updatereminder/UpdateReminderComponent.java +++ b/src/main/java/org/cryptomator/ui/updatereminder/UpdateReminderComponent.java @@ -8,7 +8,8 @@ import org.cryptomator.ui.common.FxmlScene; import javafx.scene.Scene; import javafx.stage.Stage; -import java.time.LocalDate; +import java.time.Duration; +import java.time.Instant; @UpdateReminderScoped @Subcomponent(modules = {UpdateReminderModule.class}) @@ -23,7 +24,8 @@ public interface UpdateReminderComponent { Settings settings(); default void checkAndShowUpdateReminderWindow() { - if (LocalDate.parse(settings().lastUpdateCheck.get()).isBefore(LocalDate.now().minusDays(14)) && !settings().checkForUpdates.getValue()) { + var now = Instant.now(); + if (!settings().checkForUpdates.getValue() && settings().lastSuccessfulUpdateCheck.get().isBefore(now.minus(Duration.ofDays(14)))) { Stage stage = window(); stage.setScene(updateReminderScene().get()); stage.sizeToScene(); @@ -33,6 +35,7 @@ public interface UpdateReminderComponent { @Subcomponent.Factory interface Factory { + UpdateReminderComponent create(); } } \ No newline at end of file diff --git a/src/main/java/org/cryptomator/ui/updatereminder/UpdateReminderController.java b/src/main/java/org/cryptomator/ui/updatereminder/UpdateReminderController.java index 28ae0b5c6..183298c44 100644 --- a/src/main/java/org/cryptomator/ui/updatereminder/UpdateReminderController.java +++ b/src/main/java/org/cryptomator/ui/updatereminder/UpdateReminderController.java @@ -7,8 +7,6 @@ import org.cryptomator.ui.fxapp.UpdateChecker; import javax.inject.Inject; import javafx.fxml.FXML; import javafx.stage.Stage; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; @UpdateReminderScoped public class UpdateReminderController implements FxController { @@ -27,20 +25,17 @@ public class UpdateReminderController implements FxController { @FXML public void cancel() { - settings.lastUpdateCheck.set(LocalDate.now().format(DateTimeFormatter.ISO_DATE)); window.close(); } @FXML public void once() { - settings.lastUpdateCheck.set(LocalDate.now().format(DateTimeFormatter.ISO_DATE)); updateChecker.checkForUpdatesNow(); window.close(); } @FXML public void automatically() { - settings.lastUpdateCheck.set(LocalDate.now().format(DateTimeFormatter.ISO_DATE)); updateChecker.checkForUpdatesNow(); settings.checkForUpdates.set(true); window.close(); diff --git a/src/main/resources/fxml/preferences_updates.fxml b/src/main/resources/fxml/preferences_updates.fxml index 3156d1c3c..d0910949b 100644 --- a/src/main/resources/fxml/preferences_updates.fxml +++ b/src/main/resources/fxml/preferences_updates.fxml @@ -1,13 +1,19 @@ + + + + - + + + - - + - + - - + + - - - + + + + + + + + + + + + + + + diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index 1585448e5..acdac613b 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -321,6 +321,14 @@ preferences.updates.currentVersion=Current Version: %s preferences.updates.autoUpdateCheck=Check for updates automatically preferences.updates.checkNowBtn=Check Now preferences.updates.updateAvailable=Update to version %s available. +preferences.updates.lastUpdateCheck=Last check: %s +preferences.updates.lastUpdateCheck.never=never +preferences.updates.lastUpdateCheck.recently=recently +preferences.updates.lastUpdateCheck.daysAgo=%s days ago +preferences.updates.lastUpdateCheck.hoursAgo=%s hours ago +preferences.updates.checkFailed=Looking for updates failed. Please check your internet connection or try again later. +preferences.updates.upToDate=Cryptomator is up-to-date. + ## Contribution preferences.contribute=Support Us preferences.contribute.registeredFor=Supporter certificate registered for %s diff --git a/src/test/java/org/cryptomator/common/settings/SettingsJsonTest.java b/src/test/java/org/cryptomator/common/settings/SettingsJsonTest.java index da78ee5bc..a73431681 100644 --- a/src/test/java/org/cryptomator/common/settings/SettingsJsonTest.java +++ b/src/test/java/org/cryptomator/common/settings/SettingsJsonTest.java @@ -3,6 +3,7 @@ package org.cryptomator.common.settings; import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -68,7 +69,7 @@ public class SettingsJsonTest { jsonObj.theme = UiTheme.DARK; jsonObj.showTrayIcon = false; - var jsonStr = new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(jsonObj); + var jsonStr = new ObjectMapper().registerModule(new JavaTimeModule()).writerWithDefaultPrettyPrinter().writeValueAsString(jsonObj); MatcherAssert.assertThat(jsonStr, containsString("\"theme\" : \"DARK\"")); MatcherAssert.assertThat(jsonStr, containsString("\"showTrayIcon\" : false"));