remove lastUpdateCheck from Settings; add lastUpdateReminder and lastSuccessfulUpdateCheck

implement updateTimeDifferenceMessage
reorder UI elements
refactor code for clarity and maintainability
This commit is contained in:
Jan-Peter Klein 2024-04-17 11:40:58 +02:00
parent 853ea69180
commit 98bcf63b2c
No known key found for this signature in database
GPG Key ID: 90EDA3A7C822FD0E
8 changed files with 85 additions and 53 deletions

View File

@ -25,10 +25,6 @@ import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.geometry.NodeOrientation; import javafx.geometry.NodeOrientation;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.function.Consumer; import java.util.function.Consumer;
public class Settings { public class Settings {
@ -48,8 +44,8 @@ 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_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 String DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT.name();
static final boolean DEFAULT_SHOW_MINIMIZE_BUTTON = false; static final boolean DEFAULT_SHOW_MINIMIZE_BUTTON = false;
public static final String DEFAULT_LAST_UPDATE_CHECK = "2000-01-01T10:00:00"; static final String DEFAULT_LAST_UPDATE_REMINDER = "2000-01-01";
public static final String DEFAULT_LAST_SUCCESSFUL_UPDATE_CHECK = "2000-01-01T00:00:00";
public final ObservableList<VaultSettings> directories; public final ObservableList<VaultSettings> directories;
public final BooleanProperty askedForUpdateCheck; public final BooleanProperty askedForUpdateCheck;
public final BooleanProperty checkForUpdates; public final BooleanProperty checkForUpdates;
@ -71,7 +67,8 @@ public class Settings {
public final IntegerProperty windowHeight; public final IntegerProperty windowHeight;
public final StringProperty language; public final StringProperty language;
public final StringProperty mountService; public final StringProperty mountService;
public final StringProperty lastUpdateCheck; public final StringProperty lastUpdateReminder;
public final StringProperty lastSuccessfulUpdateCheck;
public final StringProperty latestVersion; public final StringProperty latestVersion;
private Consumer<Settings> saveCmd; private Consumer<Settings> saveCmd;
@ -109,7 +106,8 @@ public class Settings {
this.windowHeight = new SimpleIntegerProperty(this, "windowHeight", json.windowHeight); this.windowHeight = new SimpleIntegerProperty(this, "windowHeight", json.windowHeight);
this.language = new SimpleStringProperty(this, "language", json.language); this.language = new SimpleStringProperty(this, "language", json.language);
this.mountService = new SimpleStringProperty(this, "mountService", json.mountService); this.mountService = new SimpleStringProperty(this, "mountService", json.mountService);
this.lastUpdateCheck = new SimpleStringProperty(this, "lastUpdateCheck", json.lastUpdateCheck); this.lastUpdateReminder = new SimpleStringProperty(this, "lastUpdateReminder", json.lastUpdateReminder);
this.lastSuccessfulUpdateCheck = new SimpleStringProperty(this, "lastSuccessfulUpdateCheck", json.lastSuccessfulUpdateCheck);
this.latestVersion = new SimpleStringProperty(this, "latestVersion", json.latestVersion); this.latestVersion = new SimpleStringProperty(this, "latestVersion", json.latestVersion);
this.directories.addAll(json.directories.stream().map(VaultSettings::new).toList()); this.directories.addAll(json.directories.stream().map(VaultSettings::new).toList());
@ -137,7 +135,8 @@ public class Settings {
windowHeight.addListener(this::somethingChanged); windowHeight.addListener(this::somethingChanged);
language.addListener(this::somethingChanged); language.addListener(this::somethingChanged);
mountService.addListener(this::somethingChanged); mountService.addListener(this::somethingChanged);
lastUpdateCheck.addListener(this::somethingChanged); lastUpdateReminder.addListener(this::somethingChanged);
lastSuccessfulUpdateCheck.addListener(this::somethingChanged);
latestVersion.addListener(this::somethingChanged); latestVersion.addListener(this::somethingChanged);
} }
@ -168,19 +167,6 @@ public class Settings {
}); });
} }
var dateTimeString = !lastUpdateCheck.get().isEmpty() ? lastUpdateCheck.get() : DEFAULT_LAST_UPDATE_CHECK;
try {
LocalDateTime dateTime = LocalDateTime.parse(dateTimeString, DateTimeFormatter.ISO_DATE_TIME);
lastUpdateCheck.set(dateTime.toString());
} catch (DateTimeParseException e) {
try {
LocalDate date = LocalDate.parse(dateTimeString, DateTimeFormatter.ISO_DATE);
lastUpdateCheck.set(LocalDateTime.of(date, LocalDate.MIN.atStartOfDay().toLocalTime()).toString());
} catch (DateTimeParseException ex) {
LOG.error("The date/time format is invalid:" + dateTimeString, ex);
}
}
} }
SettingsJson serialized() { SettingsJson serialized() {
@ -206,7 +192,8 @@ public class Settings {
json.windowHeight = windowHeight.get(); json.windowHeight = windowHeight.get();
json.language = language.get(); json.language = language.get();
json.mountService = mountService.get(); json.mountService = mountService.get();
json.lastUpdateCheck = lastUpdateCheck.get(); json.lastUpdateReminder = lastUpdateReminder.get();
json.lastSuccessfulUpdateCheck = lastSuccessfulUpdateCheck.get();
json.latestVersion = latestVersion.get(); json.latestVersion = latestVersion.get();
return json; return json;
} }

View File

@ -80,8 +80,11 @@ 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 @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; String preferredVolumeImpl;
@JsonProperty("lastUpdateCheck") @JsonProperty("lastUpdateReminder")
String lastUpdateCheck = Settings.DEFAULT_LAST_UPDATE_CHECK; String lastUpdateReminder = Settings.DEFAULT_LAST_UPDATE_REMINDER;
@JsonProperty("lastSuccessfulUpdateCheck")
String lastSuccessfulUpdateCheck = Settings.DEFAULT_LAST_SUCCESSFUL_UPDATE_CHECK;
@JsonProperty("latestVersion") @JsonProperty("latestVersion")
String latestVersion; String latestVersion;

View File

@ -20,6 +20,7 @@ import javafx.concurrent.WorkerStateEvent;
import javafx.util.Duration; import javafx.util.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Comparator; import java.util.Comparator;
import java.util.ResourceBundle;
@FxApplicationScoped @FxApplicationScoped
public class UpdateChecker { public class UpdateChecker {
@ -29,29 +30,36 @@ public class UpdateChecker {
private final Environment env; private final Environment env;
private final Settings settings; private final Settings settings;
private final ResourceBundle resourceBundle;
private final StringProperty latestVersionProperty = new SimpleStringProperty(); private final StringProperty latestVersionProperty = new SimpleStringProperty();
private final ScheduledService<String> updateCheckerService; private final ScheduledService<String> updateCheckerService;
private final ObjectProperty<UpdateCheckState> state = new SimpleObjectProperty<>(UpdateCheckState.NOT_CHECKED); private final ObjectProperty<UpdateCheckState> state = new SimpleObjectProperty<>(UpdateCheckState.NOT_CHECKED);
private final ObjectProperty<LocalDateTime> updateCheckTimeProperty = new SimpleObjectProperty<>(); private final ObjectProperty<LocalDateTime> updateCheckTimeProperty = new SimpleObjectProperty<>();
private final StringProperty timeDifferenceMessageProperty = new SimpleStringProperty();
private final Comparator<String> versionComparator = new SemVerComparator(); private final Comparator<String> versionComparator = new SemVerComparator();
private final BooleanBinding updateAvailable; private final BooleanBinding updateAvailable;
@Inject @Inject
UpdateChecker(Settings settings, // UpdateChecker(Settings settings, //
Environment env, // Environment env, //
ResourceBundle resourceBundle, //
ScheduledService<String> updateCheckerService) { ScheduledService<String> updateCheckerService) {
this.env = env; this.env = env;
this.settings = settings; this.settings = settings;
this.resourceBundle = resourceBundle;
this.updateCheckerService = updateCheckerService; this.updateCheckerService = updateCheckerService;
this.latestVersionProperty.set(settings.latestVersion.get()); this.latestVersionProperty.set(settings.latestVersion.get());
this.updateCheckTimeProperty.set(LocalDateTime.parse(settings.lastUpdateCheck.get())); this.updateCheckTimeProperty.set(LocalDateTime.parse(settings.lastSuccessfulUpdateCheck.get()));
this.updateAvailable = Bindings.createBooleanBinding(() -> { this.updateAvailable = Bindings.createBooleanBinding(() -> {
var latestVersion = latestVersionProperty.get(); var latestVersion = latestVersionProperty.get();
return latestVersion != null && versionComparator.compare(getCurrentVersion(), latestVersion) < 0; return latestVersion != null && versionComparator.compare(getCurrentVersion(), latestVersion) < 0;
}, latestVersionProperty); }, latestVersionProperty);
updateTimeDifferenceMessage();
this.latestVersionProperty.addListener((_, _, newValue) -> settings.latestVersion.set(newValue)); this.latestVersionProperty.addListener((_, _, newValue) -> settings.latestVersion.set(newValue));
this.updateCheckTimeProperty.addListener((_, _, newValue) -> settings.lastUpdateCheck.set(newValue.toString())); this.updateCheckTimeProperty.addListener((_, _, newValue) -> settings.lastSuccessfulUpdateCheck.set(newValue.toString()));
} }
public void automaticallyCheckForUpdatesIfEnabled() { public void automaticallyCheckForUpdatesIfEnabled() {
@ -74,6 +82,27 @@ public class UpdateChecker {
updateCheckerService.start(); updateCheckerService.start();
} }
private void updateTimeDifferenceMessage() {
LocalDateTime updateCheckDate = updateCheckTimeProperty.get();
if (updateCheckDate == null || updateCheckDate.equals(LocalDateTime.parse(Settings.DEFAULT_LAST_SUCCESSFUL_UPDATE_CHECK))) {
timeDifferenceMessageProperty.set(resourceBundle.getString("preferences.updates.lastUpdateCheck.never"));
return;
}
var duration = java.time.Duration.between(updateCheckDate, LocalDateTime.now());
var hours = duration.toHours();
var days = duration.toDays();
if (hours < 1) {
timeDifferenceMessageProperty.set(resourceBundle.getString("preferences.updates.lastUpdateCheck.recently"));
} else if (hours < 24) {
timeDifferenceMessageProperty.set(String.format(resourceBundle.getString("preferences.updates.lastUpdateCheck.hoursAgo"), hours));
} else {
timeDifferenceMessageProperty.set(String.format(resourceBundle.getString("preferences.updates.lastUpdateCheck.daysAgo"), days));
}
}
private void checkStarted(WorkerStateEvent event) { private void checkStarted(WorkerStateEvent event) {
LOG.debug("Checking for updates..."); LOG.debug("Checking for updates...");
state.set(UpdateCheckState.IS_CHECKING); state.set(UpdateCheckState.IS_CHECKING);
@ -83,13 +112,14 @@ public class UpdateChecker {
String latestVersion = updateCheckerService.getValue(); String latestVersion = updateCheckerService.getValue();
LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), latestVersion); LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), latestVersion);
updateCheckTimeProperty.set(LocalDateTime.now()); updateCheckTimeProperty.set(LocalDateTime.now());
updateTimeDifferenceMessage();
latestVersionProperty.set(latestVersion); latestVersionProperty.set(latestVersion);
state.set(UpdateCheckState.CHECK_SUCCESSFUL); state.set(UpdateCheckState.CHECK_SUCCESSFUL);
} }
private void checkFailed(WorkerStateEvent event) { private void checkFailed(WorkerStateEvent event) {
LOG.warn("Error checking for updates", event.getSource().getException());
state.set(UpdateCheckState.CHECK_FAILED); state.set(UpdateCheckState.CHECK_FAILED);
LOG.warn("Error checking for updates", event.getSource().getException());
} }
public enum UpdateCheckState { public enum UpdateCheckState {
@ -108,7 +138,6 @@ public class UpdateChecker {
return latestVersionProperty; return latestVersionProperty;
} }
public BooleanBinding updateAvailableProperty(){ public BooleanBinding updateAvailableProperty(){
return updateAvailable; return updateAvailable;
} }
@ -120,6 +149,10 @@ public class UpdateChecker {
return updateCheckTimeProperty; return updateCheckTimeProperty;
} }
public StringProperty timeDifferenceMessageProperty() {
return timeDifferenceMessageProperty;
}
public ObjectProperty<UpdateCheckState> updateCheckStateProperty() { public ObjectProperty<UpdateCheckState> updateCheckStateProperty() {
return state; return state;
} }

View File

@ -39,6 +39,7 @@ public class UpdatesPreferencesController implements FxController {
private final ObjectBinding<ContentDisplay> checkForUpdatesButtonState; private final ObjectBinding<ContentDisplay> checkForUpdatesButtonState;
private final ReadOnlyStringProperty latestVersion; private final ReadOnlyStringProperty latestVersion;
private final ObjectProperty<LocalDateTime> updateCheckDate; private final ObjectProperty<LocalDateTime> updateCheckDate;
private final ReadOnlyStringProperty timeDifferenceMessage;
private final String currentVersion; private final String currentVersion;
private final BooleanBinding updateAvailable; private final BooleanBinding updateAvailable;
private final BooleanProperty upToDateLabelVisible = new SimpleBooleanProperty(false); private final BooleanProperty upToDateLabelVisible = new SimpleBooleanProperty(false);
@ -58,6 +59,7 @@ public class UpdatesPreferencesController implements FxController {
this.checkForUpdatesButtonState = Bindings.when(updateChecker.checkingForUpdatesProperty()).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY); this.checkForUpdatesButtonState = Bindings.when(updateChecker.checkingForUpdatesProperty()).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY);
this.latestVersion = updateChecker.latestVersionProperty(); this.latestVersion = updateChecker.latestVersionProperty();
this.updateCheckDate = updateChecker.updateCheckTimeProperty(); this.updateCheckDate = updateChecker.updateCheckTimeProperty();
this.timeDifferenceMessage = updateChecker.timeDifferenceMessageProperty();
this.currentVersion = updateChecker.getCurrentVersion(); this.currentVersion = updateChecker.getCurrentVersion();
this.updateAvailable = updateChecker.updateAvailableProperty(); this.updateAvailable = updateChecker.updateAvailableProperty();
this.updateCheckStateProperty = updateChecker.updateCheckStateProperty(); this.updateCheckStateProperty = updateChecker.updateCheckStateProperty();
@ -68,12 +70,11 @@ public class UpdatesPreferencesController implements FxController {
BooleanBinding isUpdateSuccessfulAndCurrent = updateCheckStateProperty.isEqualTo(UpdateChecker.UpdateCheckState.CHECK_SUCCESSFUL).and(latestVersion.isEqualTo(currentVersion)); BooleanBinding isUpdateSuccessfulAndCurrent = updateCheckStateProperty.isEqualTo(UpdateChecker.UpdateCheckState.CHECK_SUCCESSFUL).and(latestVersion.isEqualTo(currentVersion));
updateCheckStateProperty.addListener((_, _, _) -> { updateCheckStateProperty.addListener((_, _, _) -> {
if (isUpdateSuccessfulAndCurrent.get()) { if (isUpdateSuccessfulAndCurrent.get()) {
upToDateLabelVisibleProperty().set(true); upToDateLabelVisible.set(true);
PauseTransition delay = new PauseTransition(Duration.seconds(5)); PauseTransition delay = new PauseTransition(Duration.seconds(5));
delay.setOnFinished(_ -> upToDateLabelVisibleProperty().set(false)); delay.setOnFinished(_ -> upToDateLabelVisible.set(false));
delay.play(); delay.play();
} }
}); });
@ -122,15 +123,23 @@ public class UpdatesPreferencesController implements FxController {
public String getUpdateCheckDate() { public String getUpdateCheckDate() {
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault()); DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
return !updateCheckDate.get().equals(LocalDateTime.parse(Settings.DEFAULT_LAST_UPDATE_CHECK)) ? updateCheckDate.get().format(formatter) : "-"; return !updateCheckDate.get().equals(LocalDateTime.parse(Settings.DEFAULT_LAST_SUCCESSFUL_UPDATE_CHECK)) ? updateCheckDate.get().format(formatter) : "-";
}
public ReadOnlyStringProperty timeDifferenceMessageProperty(){
return timeDifferenceMessage;
}
public String getTimeDifferenceMessage() {
return timeDifferenceMessage.get();
} }
public BooleanProperty upToDateLabelVisibleProperty() { public BooleanProperty upToDateLabelVisibleProperty() {
return upToDateLabelVisible; return upToDateLabelVisible;
} }
public final boolean isUpToDateLabelVisible() { public boolean isUpToDateLabelVisible() {
return upToDateLabelVisibleProperty().get(); return upToDateLabelVisible.get();
} }
public BooleanBinding updateAvailableProperty() { public BooleanBinding updateAvailableProperty() {

View File

@ -10,9 +10,7 @@ import org.slf4j.LoggerFactory;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.time.LocalDateTime; import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
@UpdateReminderScoped @UpdateReminderScoped
@Subcomponent(modules = {UpdateReminderModule.class}) @Subcomponent(modules = {UpdateReminderModule.class})
@ -29,16 +27,12 @@ public interface UpdateReminderComponent {
Settings settings(); Settings settings();
default void checkAndShowUpdateReminderWindow() { default void checkAndShowUpdateReminderWindow() {
try { if (LocalDate.parse(settings().lastUpdateReminder.get()).isBefore(LocalDate.now().minusDays(14)) && !settings().checkForUpdates.getValue()) {
var dateTime = LocalDateTime.parse(settings().lastUpdateCheck.get(), DateTimeFormatter.ISO_DATE_TIME); settings().lastUpdateReminder.set(LocalDate.now().toString());
if (dateTime.isBefore(LocalDateTime.now().minusDays(14)) && !settings().checkForUpdates.getValue()) { Stage stage = window();
Stage stage = window(); stage.setScene(updateReminderScene().get());
stage.setScene(updateReminderScene().get()); stage.sizeToScene();
stage.sizeToScene(); stage.show();
stage.show();
}
} catch (DateTimeParseException e) {
LOG.error("Failed to parse last update check time '{}':", settings().lastUpdateCheck.get(), e);
} }
} }

View File

@ -28,7 +28,6 @@ public class UpdateReminderController implements FxController {
@FXML @FXML
public void cancel() { public void cancel() {
updateChecker.updateCheckTimeProperty().set(LocalDateTime.now());
window.close(); window.close();
} }

View File

@ -11,6 +11,7 @@
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Tooltip?>
<VBox xmlns:fx="http://javafx.com/fxml" <VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx" xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.preferences.UpdatesPreferencesController" fx:controller="org.cryptomator.ui.preferences.UpdatesPreferencesController"
@ -22,8 +23,6 @@
<Insets topRightBottomLeft="24"/> <Insets topRightBottomLeft="24"/>
</padding> </padding>
<FormattedLabel format="%preferences.updates.currentVersion" arg1="${controller.currentVersion}" textAlignment="CENTER" wrapText="true"/> <FormattedLabel format="%preferences.updates.currentVersion" arg1="${controller.currentVersion}" textAlignment="CENTER" wrapText="true"/>
<FormattedLabel format="%preferences.updates.latestVersion" arg1="${controller.latestVersion}" textAlignment="CENTER" wrapText="true"/>
<FormattedLabel format="%preferences.updates.lastUpdateCheck" arg1="${controller.updateCheckDate}" textAlignment="CENTER" wrapText="true"/>
<CheckBox fx:id="checkForUpdatesCheckbox" text="%preferences.updates.autoUpdateCheck"/> <CheckBox fx:id="checkForUpdatesCheckbox" text="%preferences.updates.autoUpdateCheck"/>
@ -42,6 +41,11 @@
</Label> </Label>
<Hyperlink styleClass="hyperlink-underline" text="%preferences.general.debugDirectory" onAction="#showLogfileDirectory"/> <Hyperlink styleClass="hyperlink-underline" text="%preferences.general.debugDirectory" onAction="#showLogfileDirectory"/>
</HBox> </HBox>
<FormattedLabel format="%preferences.updates.lastUpdateCheck" arg1="${controller.timeDifferenceMessage}" textAlignment="CENTER" wrapText="true">
<tooltip>
<Tooltip text="${controller.updateCheckDate}" showDelay="10ms"/>
</tooltip>
</FormattedLabel>
<Label fx:id="upToDateLabel" text="%preferences.updates.upToDate" visible="${controller.upToDateLabelVisible}" managed="${controller.upToDateLabelVisible}"> <Label fx:id="upToDateLabel" text="%preferences.updates.upToDate" visible="${controller.upToDateLabelVisible}" managed="${controller.upToDateLabelVisible}">
<graphic> <graphic>
<FontAwesome5IconView glyphSize="12" styleClass="glyph-icon-primary" glyph="CHECK"/> <FontAwesome5IconView glyphSize="12" styleClass="glyph-icon-primary" glyph="CHECK"/>

View File

@ -318,11 +318,14 @@ preferences.volume.feature.readOnly=Read-only mount
## Updates ## Updates
preferences.updates=Updates preferences.updates=Updates
preferences.updates.currentVersion=Current Version: %s preferences.updates.currentVersion=Current Version: %s
preferences.updates.latestVersion=Latest Version: %s
preferences.updates.autoUpdateCheck=Check for updates automatically preferences.updates.autoUpdateCheck=Check for updates automatically
preferences.updates.checkNowBtn=Check Now preferences.updates.checkNowBtn=Check Now
preferences.updates.updateAvailable=Update to version %s available. preferences.updates.updateAvailable=Update to version %s available.
preferences.updates.lastUpdateCheck=Last update check: %s 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=Check failed preferences.updates.checkFailed=Check failed
preferences.updates.upToDate=Cryptomator is up-to-date. preferences.updates.upToDate=Cryptomator is up-to-date.