Quellcode durchsuchen

Merge branch 'develop' into feature/dokany-info-dialog

Jan-Peter Klein vor 1 Jahr
Ursprung
Commit
58fe6da7a8
38 geänderte Dateien mit 285 neuen und 147 gelöschten Zeilen
  1. 2 2
      .github/workflows/appimage.yml
  2. 1 1
      .github/workflows/build.yml
  3. 1 1
      .github/workflows/check-jdk-updates.yml
  4. 3 3
      .github/workflows/debian.yml
  5. 1 1
      .github/workflows/dependency-check.yml
  6. 1 1
      .github/workflows/get-version.yml
  7. 2 2
      .github/workflows/mac-dmg.yml
  8. 1 1
      .github/workflows/pullrequest.yml
  9. 1 1
      .github/workflows/release-check.yml
  10. 2 2
      .github/workflows/win-exe.yml
  11. 1 1
      .idea/misc.xml
  12. 3 1
      dist/linux/appimage/.gitignore
  13. 1 1
      dist/linux/appimage/build.sh
  14. 1 1
      dist/linux/debian/control
  15. 2 2
      dist/linux/debian/rules
  16. 4 2
      dist/mac/dmg/.gitignore
  17. 1 1
      dist/mac/dmg/build.sh
  18. 1 0
      dist/win/.gitignore
  19. 1 1
      dist/win/build.ps1
  20. 0 5
      dist/win/contrib/version170-migrate-settings.bat
  21. 0 35
      dist/win/contrib/version170-migrate-settings.ps1
  22. 0 6
      dist/win/resources/main.wxs
  23. 23 11
      pom.xml
  24. 1 1
      src/main/java/module-info.java
  25. 2 1
      src/main/java/org/cryptomator/common/SubstitutingProperties.java
  26. 6 6
      src/main/java/org/cryptomator/common/settings/Settings.java
  27. 5 2
      src/main/java/org/cryptomator/common/settings/SettingsJson.java
  28. 2 1
      src/main/java/org/cryptomator/common/settings/SettingsProvider.java
  29. 62 21
      src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java
  30. 0 9
      src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java
  31. 1 1
      src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java
  32. 7 0
      src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java
  33. 97 2
      src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java
  34. 5 2
      src/main/java/org/cryptomator/ui/updatereminder/UpdateReminderComponent.java
  35. 0 5
      src/main/java/org/cryptomator/ui/updatereminder/UpdateReminderController.java
  36. 34 13
      src/main/resources/fxml/preferences_updates.fxml
  37. 8 0
      src/main/resources/i18n/strings.properties
  38. 2 1
      src/test/java/org/cryptomator/common/settings/SettingsJsonTest.java

+ 2 - 2
.github/workflows/appimage.yml

@@ -11,7 +11,7 @@ on:
 
 env:
   JAVA_DIST: 'zulu'
-  JAVA_VERSION: '21.0.2+13'
+  JAVA_VERSION: '22.0.1+8'
 
 jobs:
   get-version:
@@ -80,7 +80,7 @@ jobs:
           --verbose
           --output runtime
           --module-path "${JAVA_HOME}/jmods:openjfx-jmods"
-          --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net
+          --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net
           --strip-native-commands
           --no-header-files
           --no-man-pages

+ 1 - 1
.github/workflows/build.yml

@@ -7,7 +7,7 @@ on:
 
 env:
   JAVA_DIST: 'zulu'
-  JAVA_VERSION: 21
+  JAVA_VERSION: 22
 
 defaults:
   run:

+ 1 - 1
.github/workflows/check-jdk-updates.yml

@@ -5,7 +5,7 @@ on:
     - cron: '0 0 1 * *' # run once a month at the first day of month
 
 env:
-  JDK_VERSION: '21.0.2+13'
+  JDK_VERSION: '22.0.1+8'
   JDK_VENDOR: zulu
 
 jobs:

+ 3 - 3
.github/workflows/debian.yml

@@ -17,9 +17,9 @@ on:
 
 env:
   JAVA_DIST: 'zulu'
-  JAVA_VERSION: '21.0.2+13'
-  COFFEELIBS_JDK: 21
-  COFFEELIBS_JDK_VERSION: '21.0.2+13-0ppa1'
+  JAVA_VERSION: '22.0.1+8'
+  COFFEELIBS_JDK: 22
+  COFFEELIBS_JDK_VERSION: '22.0.1+8-0ppa1'
   OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_linux-x64_bin-jmods.zip'
   OPENJFX_JMODS_AMD64_HASH: '7baed11ca56d5fee85995fa6612d4299f1e8b7337287228f7f12fd50407c56f8'
   OPENJFX_JMODS_AARCH64: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_linux-aarch64_bin-jmods.zip'

+ 1 - 1
.github/workflows/dependency-check.yml

@@ -11,7 +11,7 @@ jobs:
     with:
       runner-os: 'ubuntu-latest'
       java-distribution: 'temurin'
-      java-version: 21
+      java-version: 22
     secrets:
       nvd-api-key: ${{ secrets.NVD_API_KEY }}
       slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 1 - 1
.github/workflows/get-version.yml

@@ -23,7 +23,7 @@ on:
 
 env:
   JAVA_DIST: 'zulu'
-  JAVA_VERSION: 21
+  JAVA_VERSION: 22
 
 jobs:
   determine-version:

+ 2 - 2
.github/workflows/mac-dmg.yml

@@ -16,7 +16,7 @@ on:
 
 env:
   JAVA_DIST: 'zulu'
-  JAVA_VERSION: '21.0.2+13'
+  JAVA_VERSION: '22.0.1+8'
 
 jobs:
   get-version:
@@ -91,7 +91,7 @@ jobs:
           --verbose
           --output runtime
           --module-path "${JAVA_HOME}/jmods:openjfx-jmods"
-          --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
+          --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.accessibility,jdk.management.jfr
           --strip-native-commands
           --no-header-files
           --no-man-pages

+ 1 - 1
.github/workflows/pullrequest.yml

@@ -5,7 +5,7 @@ on:
 
 env:
   JAVA_DIST: 'zulu'
-  JAVA_VERSION: 21
+  JAVA_VERSION: 22
 
 defaults:
   run:

+ 1 - 1
.github/workflows/release-check.yml

@@ -12,7 +12,7 @@ defaults:
 
 env:
   JAVA_DIST: 'zulu'
-  JAVA_VERSION: 21
+  JAVA_VERSION: 22
 
 jobs:
   check-preconditions:

+ 2 - 2
.github/workflows/win-exe.yml

@@ -16,7 +16,7 @@ on:
 
 env:
   JAVA_DIST: 'zulu'
-  JAVA_VERSION: '21.0.2+13'
+  JAVA_VERSION: '22.0.1+8'
   OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_windows-x64_bin-jmods.zip'
   OPENJFX_JMODS_AMD64_HASH: 'daf8acae631c016c24cfe23f88469400274d3441dd890615a42dfb501f3eb94a'
   WINFSP_MSI: 'https://github.com/winfsp/winfsp/releases/download/v2.0/winfsp-2.0.23075.msi'
@@ -89,7 +89,7 @@ jobs:
           --verbose
           --output runtime
           --module-path "jfxjmods;${JAVA_HOME}/jmods"
-          --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
+          --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.accessibility,jdk.management.jfr
           --strip-native-commands
           --no-header-files
           --no-man-pages

+ 1 - 1
.idea/misc.xml

@@ -8,7 +8,7 @@
       </list>
     </option>
   </component>
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_21_PREVIEW" project-jdk-name="21" project-jdk-type="JavaSDK">
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_22" project-jdk-name="22" project-jdk-type="JavaSDK">
     <output url="file://$PROJECT_DIR$/out" />
   </component>
 </project>

+ 3 - 1
dist/linux/appimage/.gitignore

@@ -1,4 +1,6 @@
-# created during build
+# downloaded/created during build
+openjfx-jmods.zip
+*.jmod
 Cryptomator.AppDir
 *.AppImage
 *.AppImage.zsync

+ 1 - 1
dist/linux/appimage/build.sh

@@ -56,7 +56,7 @@ ${JAVA_HOME}/bin/jlink \
     --verbose \
     --output runtime \
     --module-path "${JAVA_HOME}/jmods:openjfx-jmods" \
-    --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net \
+    --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net \
     --strip-native-commands \
     --no-header-files \
     --no-man-pages \

+ 1 - 1
dist/linux/debian/control

@@ -2,7 +2,7 @@ Source: cryptomator
 Maintainer: Cryptobot <releases@cryptomator.org>
 Section: utils
 Priority: optional
-Build-Depends: debhelper (>=10), coffeelibs-jdk-21 (>= 21.0.2+12-0ppa1), libgtk-3-0,  libxxf86vm1, libgl1
+Build-Depends: debhelper (>=10), coffeelibs-jdk-22 (>= 22.0.1+8-0ppa1), libgtk-3-0,  libxxf86vm1, libgl1
 Standards-Version: 4.5.0
 Homepage: https://cryptomator.org
 Vcs-Git: https://github.com/cryptomator/cryptomator.git

+ 2 - 2
dist/linux/debian/rules

@@ -4,7 +4,7 @@
 # Uncomment this to turn on verbose mode.
 #export DH_VERBOSE=1
 
-JAVA_HOME = /usr/lib/jvm/java-21-coffeelibs
+JAVA_HOME = /usr/lib/jvm/java-22-coffeelibs
 DEB_BUILD_ARCH ?= $(shell dpkg-architecture -qDEB_BUILD_ARCH)
 ifeq ($(DEB_BUILD_ARCH),amd64)
 JMODS_PATH = jmods/amd64:${JAVA_HOME}/jmods
@@ -28,7 +28,7 @@ override_dh_auto_build:
 	$(JAVA_HOME)/bin/jlink \
 		--output runtime \
 		--module-path "${JMODS_PATH}" \
-		--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net \
+		--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net \
 		--strip-native-commands \
 		--no-header-files \
 		--no-man-pages \

+ 4 - 2
dist/mac/dmg/.gitignore

@@ -1,6 +1,8 @@
-# created during build
+# downloaded/created during build
 Cryptomator.app/
 runtime/
 dmg/
 *.dmg
-license.rtf
+license.rtf
+openjfx-jmods.zip
+*.jmod

+ 1 - 1
dist/mac/dmg/build.sh

@@ -71,7 +71,7 @@ cp ../../../target/${MAIN_JAR_GLOB} ../../../target/mods
 ${JAVA_HOME}/bin/jlink \
     --output runtime \
     --module-path "${JAVA_HOME}/jmods:openjfx-jmods" \
-    --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.security.auth,jdk.accessibility,jdk.management.jfr \
+    --add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.security.auth,jdk.accessibility,jdk.management.jfr \
     --strip-native-commands \
     --no-header-files \
     --no-man-pages \

+ 1 - 0
dist/win/.gitignore

@@ -6,4 +6,5 @@ installer
 *.msi
 *.exe
 *.jmod
+resources/jfxJmods.zip
 license.rtf

+ 1 - 1
dist/win/build.ps1

@@ -74,7 +74,7 @@ Move-Item -Force -Path ".\resources\javafx-jmods-*" -Destination ".\resources\ja
 	--verbose `
 	--output runtime `
 	--module-path "$Env:JAVA_HOME/jmods;$buildDir/resources/javafx-jmods" `
-	--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr,javafx.base,javafx.graphics,javafx.controls,javafx.fxml `
+	--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.accessibility,jdk.management.jfr,javafx.base,javafx.graphics,javafx.controls,javafx.fxml `
 	--strip-native-commands `
 	--no-header-files `
 	--no-man-pages `

+ 0 - 5
dist/win/contrib/version170-migrate-settings.bat

@@ -1,5 +0,0 @@
-@echo off
-:: see comments in file ./version170-migrate-settings.ps1
-
-cd %~dp0
-powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy RemoteSigned -Command .\version170-migrate-settings.ps1

+ 0 - 35
dist/win/contrib/version170-migrate-settings.ps1

@@ -1,35 +0,0 @@
-# This script migrates Cryptomator settings for all local users on Windows in case the users uses custom directories as mountpoint
-# See also https://github.com/cryptomator/cryptomator/pull/2654.
-#
-# TODO: This script should be evaluated in a yearly interval if it is still needed and if not, should be removed
-#
-#Requires -RunAsAdministrator
-
-#Get all active, local user profiles
-$profileList = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'
-Get-ChildItem $profileList | ForEach-Object {
-    $profilePath =  $_.GetValue("ProfileImagePath") 
-    $settingsPath = "$profilePath\AppData\Roaming\Cryptomator\settings.json"
-    if(!(Test-Path -Path $settingsPath -PathType Leaf)) {
-        #No settings file, nothing to do.
-        return;
-    }
-    $settings = Get-Content -Path $settingsPath | ConvertFrom-Json
-    if($settings.preferredVolumeImpl -ne "FUSE") {
-        #Fuse not used, nothing to do
-        return;
-    }
-
-    #check if customMountPoints are used
-    $atLeastOneCustomPath = $false;
-    foreach ($vault in $settings.directories){
-        $atLeastOneCustomPath = $atLeastOneCustomPath -or ($vault.useCustomMountPath -eq "True")
-    }
-
-    #if so, use WinFsp Local Drive
-    if( $atLeastOneCustomPath ) {
-        Add-Member -Force -InputObject $settings -Name "mountService" -Value "org.cryptomator.frontend.fuse.mount.WinFspMountProvider" -MemberType NoteProperty
-        $newSettings  = $settings | Select-Object * -ExcludeProperty "preferredVolumeImpl"
-        ConvertTo-Json $newSettings | Set-Content -Path $settingsPath
-    }
-}

+ 0 - 6
dist/win/resources/main.wxs

@@ -139,11 +139,6 @@
             Sequence="execute" Before="PatchWebDAV" />
     <CustomAction Id="PatchWebDAV" BinaryKey="WixCA" DllEntry="WixQuietExec64" Execute="deferred" Return="ignore" Impersonate="no"/>
 
-    <!-- Special Settings migration for 1.7.0,. Should be removed eventually, for more info, see ../contrib/version170-migrate-settings.ps1-->
-    <SetProperty Id="V170MigrateSettings" Value="&quot;[INSTALLDIR]version170-migrate-settings.bat&quot;"
-            Sequence="execute" Before="V170MigrateSettings" />
-    <CustomAction Id="V170MigrateSettings" BinaryKey="WixCA" DllEntry="WixQuietExec64" Execute="deferred" Return="ignore" Impersonate="no"/>
-
     <!-- Running App detection and exit -->
     <Property Id="FOUNDRUNNINGAPP" Admin="yes"/>
     <util:CloseApplication
@@ -195,7 +190,6 @@
       <RemoveExistingProducts After="InstallValidate"/> <!-- Moved from CostInitialize, due to WixCloseApplications -->
 
       <Custom Action="PatchWebDAV" After="InstallFiles">NOT Installed OR REINSTALL</Custom>
-      <Custom Action="V170MigrateSettings" After="InstallFiles">NOT Installed OR REINSTALL</Custom>
     </InstallExecuteSequence>
 
     <InstallUISequence>

+ 23 - 11
pom.xml

@@ -26,7 +26,7 @@
 
 	<properties>
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-		<project.jdk.version>21</project.jdk.version>
+		<project.jdk.version>22</project.jdk.version>
 
 		<!-- Group IDs of jars that need to stay on the class path for now -->
 		<!-- remove them, as soon they got modularized or support is dropped (i.e., WebDAV) -->
@@ -37,23 +37,23 @@
 		<cryptomator.integrations.version>1.3.1</cryptomator.integrations.version>
 		<cryptomator.integrations.win.version>1.2.5</cryptomator.integrations.win.version>
 		<cryptomator.integrations.mac.version>1.2.3</cryptomator.integrations.mac.version>
-		<cryptomator.integrations.linux.version>1.4.4</cryptomator.integrations.linux.version>
-		<cryptomator.fuse.version>4.0.0</cryptomator.fuse.version>
+		<cryptomator.integrations.linux.version>1.4.5</cryptomator.integrations.linux.version>
+		<cryptomator.fuse.version>5.0.0</cryptomator.fuse.version>
 		<cryptomator.webdav.version>2.0.6</cryptomator.webdav.version>
 
 		<!-- 3rd party dependencies -->
 		<commons-lang3.version>3.14.0</commons-lang3.version>
-		<dagger.version>2.51</dagger.version>
+		<dagger.version>2.51.1</dagger.version>
 		<easybind.version>2.2</easybind.version>
-		<guava.version>33.0.0-jre</guava.version>
-		<jackson.version>2.16.2</jackson.version>
+		<guava.version>33.2.1-jre</guava.version>
+		<jackson.version>2.17.1</jackson.version>
 		<javafx.version>21.0.1</javafx.version>
 		<jwt.version>4.4.0</jwt.version>
 		<nimbus-jose.version>9.37.3</nimbus-jose.version>
-		<logback.version>1.5.3</logback.version>
-		<slf4j.version>2.0.12</slf4j.version>
+		<logback.version>1.5.6</logback.version>
+		<slf4j.version>2.0.13</slf4j.version>
 		<tinyoauth2.version>0.8.0</tinyoauth2.version>
-		<zxcvbn.version>1.8.2</zxcvbn.version>
+		<zxcvbn.version>1.9.0</zxcvbn.version>
 
 		<!-- test dependencies -->
 		<junit.jupiter.version>5.10.2</junit.jupiter.version>
@@ -62,7 +62,7 @@
 
 		<!-- build-time dependencies -->
 		<jetbrains.annotations.version>24.1.0</jetbrains.annotations.version>
-		<dependency-check.version>9.1.0</dependency-check.version>
+		<dependency-check.version>9.2.0</dependency-check.version>
 		<jacoco.version>0.8.12</jacoco.version>
 		<license-generator.version>2.4.0</license-generator.version>
 		<junit-tree-reporter.version>1.2.1</junit-tree-reporter.version>
@@ -70,11 +70,16 @@
 		<mvn-resources.version>3.3.1</mvn-resources.version>
 		<mvn-dependency.version>3.6.1</mvn-dependency.version>
 		<mvn-surefire.version>3.2.5</mvn-surefire.version>
-		<mvn-jar.version>3.3.0</mvn-jar.version>
+		<mvn-jar.version>3.4.1</mvn-jar.version>
 	</properties>
 
 	<dependencies>
 		<!-- Cryptomator Libs -->
+		<dependency>
+			<groupId>org.cryptomator</groupId>
+			<artifactId>cryptolib</artifactId>
+			<version>2.2.0</version>
+		</dependency>
 		<dependency>
 			<groupId>org.cryptomator</groupId>
 			<artifactId>cryptofs</artifactId>
@@ -158,11 +163,18 @@
 			<artifactId>nimbus-jose-jwt</artifactId>
 			<version>${nimbus-jose.version}</version>
 		</dependency>
+
+		<!-- Jackson -->
 		<dependency>
 			<groupId>com.fasterxml.jackson.core</groupId>
 			<artifactId>jackson-databind</artifactId>
 			<version>${jackson.version}</version>
 		</dependency>
+		<dependency>
+			<groupId>com.fasterxml.jackson.datatype</groupId>
+			<artifactId>jackson-datatype-jsr310</artifactId>
+			<version>${jackson.version}</version>
+		</dependency>
 
 		<!-- EasyBind -->
 		<dependency>

+ 1 - 1
src/main/java/module-info.java

@@ -31,13 +31,13 @@ open module org.cryptomator.desktop {
 	requires javafx.graphics;
 	requires javafx.controls;
 	requires javafx.fxml;
-	requires jdk.crypto.ec;
 	// 3rd party:
 	requires ch.qos.logback.classic;
 	requires ch.qos.logback.core;
 	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;

+ 2 - 1
src/main/java/org/cryptomator/common/SubstitutingProperties.java

@@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory;
 
 import java.util.Map;
 import java.util.Properties;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 public class SubstitutingProperties extends PropertiesDecorator {
@@ -58,7 +59,7 @@ public class SubstitutingProperties extends PropertiesDecorator {
 			LoggerFactory.getLogger(SubstitutingProperties.class).warn("Variable {} used for substitution not found in {}. Replaced with empty string.", key, src);
 			return "";
 		} else {
-			return val.replace("\\", "\\\\");
+			return Matcher.quoteReplacement(val);
 		}
 	}
 

+ 6 - 6
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<VaultSettings> 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<Instant> lastSuccessfulUpdateCheck;
 
 	private Consumer<Settings> 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;
 	}
 

+ 5 - 2
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;
 
 }

+ 2 - 1
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<Settings> {
 
-	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;
 

+ 62 - 21
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<String> semVerComparator;
+	private final StringProperty latestVersion = new SimpleStringProperty();
 	private final ScheduledService<String> updateCheckerService;
+	private final ObjectProperty<UpdateCheckState> state = new SimpleObjectProperty<>(UpdateCheckState.NOT_CHECKED);
+	private final ObjectProperty<Instant> lastSuccessfulUpdateCheck;
+	private final Comparator<String> versionComparator = new SemVerComparator();
+	private final BooleanBinding updateAvailable;
+	private final BooleanBinding checkFailed;
 
 	@Inject
-	UpdateChecker(Settings settings, Environment env, @Named("latestVersion") StringProperty latestVersionProperty, @Named("SemVer") Comparator<String> semVerComparator, ScheduledService<String> updateCheckerService) {
+	UpdateChecker(Settings settings, //
+				  Environment env, //
+				  ScheduledService<String> 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);
 	}
 
-	/* Observable Properties */
+	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<Instant> lastSuccessfulUpdateCheckProperty() {
+		return lastSuccessfulUpdateCheck;
+	}
+
+	public ObjectProperty<UpdateCheckState> updateCheckStateProperty() {
+		return state;
 	}
 
 	public String getCurrentVersion() {
 		return env.getAppVersion();
 	}
-
 }

+ 0 - 9
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<HttpClient> provideHttpClient() {

+ 1 - 1
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);

+ 7 - 0
src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java

@@ -7,6 +7,7 @@ import org.cryptomator.cryptofs.CryptoFileSystemProvider;
 import org.cryptomator.cryptofs.DirStructure;
 import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
 import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.VaultService;
 import org.cryptomator.ui.fxapp.FxApplicationWindows;
 import org.cryptomator.ui.removevault.RemoveVaultComponent;
 import org.slf4j.Logger;
@@ -58,6 +59,7 @@ public class VaultListController implements FxController {
 
 	private final Stage mainWindow;
 	private final ObservableList<Vault> vaults;
+	private final VaultService vaultService;
 	private final ObjectProperty<Vault> selectedVault;
 	private final VaultListCellFactory cellFactory;
 	private final AddVaultWizardComponent.Builder addVaultWizard;
@@ -79,6 +81,7 @@ public class VaultListController implements FxController {
 						ObservableList<Vault> vaults, //
 						ObjectProperty<Vault> selectedVault, //
 						VaultListCellFactory cellFactory, //
+						VaultService vaultService, //
 						AddVaultWizardComponent.Builder addVaultWizard, //
 						RemoveVaultComponent.Builder removeVaultDialogue, //
 						VaultListManager vaultListManager, //
@@ -88,6 +91,7 @@ public class VaultListController implements FxController {
 		this.vaults = vaults;
 		this.selectedVault = selectedVault;
 		this.cellFactory = cellFactory;
+		this.vaultService = vaultService;
 		this.addVaultWizard = addVaultWizard;
 		this.removeVaultDialogue = removeVaultDialogue;
 		this.vaultListManager = vaultListManager;
@@ -119,6 +123,9 @@ public class VaultListController implements FxController {
 				Optional.ofNullable(selectedVault.get())
 						.filter(Vault::isLocked)
 						.ifPresent(vault -> appWindows.startUnlockWorkflow(vault, mainWindow));
+				Optional.ofNullable(selectedVault.get())
+						.filter(Vault::isUnlocked)
+						.ifPresent(vaultService::reveal);
 			}
 		});
 

+ 97 - 2
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<ContentDisplay> checkForUpdatesButtonState;
 	private final ReadOnlyStringProperty latestVersion;
+	private final ObservableValue<Instant> lastSuccessfulUpdateCheck;
+	private final StringBinding lastUpdateCheckMessage;
+	private final ObservableValue<String> 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<ContentDisplay> 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<String> 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();
+	}
+
 }

+ 5 - 2
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();
 	}
 }

+ 0 - 5
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();

+ 34 - 13
src/main/resources/fxml/preferences_updates.fxml

@@ -1,13 +1,19 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
+<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
+<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
 <?import org.cryptomator.ui.controls.FormattedLabel?>
 <?import org.cryptomator.ui.controls.FormattedString?>
 <?import javafx.geometry.Insets?>
 <?import javafx.scene.control.Button?>
 <?import javafx.scene.control.CheckBox?>
 <?import javafx.scene.control.Hyperlink?>
+<?import javafx.scene.control.Label?>
+<?import javafx.scene.layout.HBox?>
 <?import javafx.scene.layout.VBox?>
-<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
+<?import javafx.scene.control.Tooltip?>
+<?import javafx.scene.text.TextFlow?>
+<?import javafx.scene.text.Text?>
 <VBox xmlns:fx="http://javafx.com/fxml"
 	  xmlns="http://javafx.com/javafx"
 	  fx:controller="org.cryptomator.ui.preferences.UpdatesPreferencesController"
@@ -18,19 +24,34 @@
 	<padding>
 		<Insets topRightBottomLeft="24"/>
 	</padding>
-	<children>
-		<FormattedLabel format="%preferences.updates.currentVersion" arg1="${controller.currentVersion}" textAlignment="CENTER" wrapText="true"/>
+	<FormattedLabel format="%preferences.updates.currentVersion" arg1="${controller.currentVersion}" textAlignment="CENTER" wrapText="true"/>
 
-		<CheckBox fx:id="checkForUpdatesCheckbox" text="%preferences.updates.autoUpdateCheck"/>
+	<CheckBox fx:id="checkForUpdatesCheckbox" text="%preferences.updates.autoUpdateCheck"/>
 
-		<VBox alignment="CENTER" spacing="12">
-			<Button text="%preferences.updates.checkNowBtn" defaultButton="true" onAction="#checkNow" contentDisplay="${controller.checkForUpdatesButtonState}">
-				<graphic>
-					<FontAwesome5Spinner fx:id="spinner" glyphSize="12"/>
-				</graphic>
-			</Button>
+	<VBox alignment="CENTER" spacing="12">
+		<Button text="%preferences.updates.checkNowBtn" defaultButton="true" onAction="#checkNow" contentDisplay="${controller.checkForUpdatesButtonState}">
+			<graphic>
+				<FontAwesome5Spinner glyphSize="12"/>
+			</graphic>
+		</Button>
 
-			<Hyperlink text="${linkLabel.value}" onAction="#visitDownloadsPage" textAlignment="CENTER" wrapText="true" styleClass="hyperlink-underline" visible="${controller.updateAvailable}"/>
-		</VBox>
-	</children>
+		<TextFlow styleClass="text-flow" textAlignment="CENTER" visible="${controller.checkFailed}" managed="${controller.checkFailed}">
+			<FontAwesome5IconView glyphSize="12" styleClass="glyph-icon-orange" glyph="EXCLAMATION_TRIANGLE"/>
+			<Text text=" "/>
+			<Text text="%preferences.updates.checkFailed"/>
+			<Text text=" "/>
+			<Hyperlink styleClass="hyperlink-underline" text="%preferences.general.debugDirectory" onAction="#showLogfileDirectory"/>
+		</TextFlow>
+		<FormattedLabel format="%preferences.updates.lastUpdateCheck" arg1="${controller.timeDifferenceMessage}" textAlignment="CENTER" wrapText="true">
+			<tooltip>
+				<Tooltip text="${controller.lastUpdateCheckMessage}" showDelay="10ms"/>
+			</tooltip>
+		</FormattedLabel>
+		<Label text="%preferences.updates.upToDate" visible="${controller.upToDateLabelVisible}" managed="${controller.upToDateLabelVisible}">
+			<graphic>
+				<FontAwesome5IconView glyphSize="12" styleClass="glyph-icon-primary" glyph="CHECK"/>
+			</graphic>
+		</Label>
+		<Hyperlink text="${linkLabel.value}" onAction="#visitDownloadsPage" textAlignment="CENTER" wrapText="true" styleClass="hyperlink-underline" visible="${controller.updateAvailable}" managed="${controller.updateAvailable}"/>
+	</VBox>
 </VBox>

+ 8 - 0
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

+ 2 - 1
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"));