mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-21 12:11:28 +00:00
Merge branch 'develop' into feature/files-in-use
# Conflicts: # pom.xml
This commit is contained in:
16
.github/workflows/av-whitelist.yml
vendored
16
.github/workflows/av-whitelist.yml
vendored
@@ -7,6 +7,16 @@ on:
|
||||
description: "Url to the file to upload"
|
||||
required: true
|
||||
type: string
|
||||
avast:
|
||||
description: "Upload to Avast"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
kaspersky:
|
||||
description: "Upload to Kaspersky"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
url:
|
||||
@@ -39,7 +49,7 @@ jobs:
|
||||
url="${INPUT_URL}"
|
||||
echo "fileName=${url##*/}" >> $GITHUB_OUTPUT
|
||||
- name: Download file
|
||||
run: curl --remote-name ${INPUT_URL} -L -o ${{steps.extractName.outputs.fileName}}
|
||||
run: curl "${INPUT_URL}" -L -o "${{steps.extractName.outputs.fileName}}" --fail-with-body
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
@@ -50,7 +60,7 @@ jobs:
|
||||
name: Anti Virus Allowlisting Kaspersky
|
||||
runs-on: ubuntu-latest
|
||||
needs: download-file
|
||||
if: github.event_name == 'workflow_call' || inputs.kaspersky
|
||||
if: inputs.kaspersky
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
@@ -70,7 +80,7 @@ jobs:
|
||||
name: Anti Virus Allowlisting Avast
|
||||
runs-on: ubuntu-latest
|
||||
needs: download-file
|
||||
if: github.event_name == 'workflow_call' || inputs.avast
|
||||
if: inputs.avast
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
|
||||
25
.github/workflows/build.yml
vendored
25
.github/workflows/build.yml
vendored
@@ -56,21 +56,21 @@ jobs:
|
||||
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
|
||||
generate_release_notes: true
|
||||
body: |-
|
||||
:construction: Work in Progress
|
||||
> [!NOTE]
|
||||
> 🚧 Work in Progress 🚧
|
||||
>
|
||||
> Please be patient, the [builds are still running](https://github.com/cryptomator/cryptomator/actions). Binary packages can be found here in a few moments.
|
||||
|
||||
<!--REPLACE with auto-generated release notes (see below)
|
||||
### What's New 🎉
|
||||
|
||||
### Bugfixes 🐛
|
||||
|
||||
### Other Changes 📎
|
||||
END REPLACE-->
|
||||
|
||||
Feel free to also read our [CHANGELOG.md](https://github.com/cryptomator/cryptomator/blob/develop/CHANGELOG.md).
|
||||
|
||||
---
|
||||
|
||||
TODO FULL CHANGELOG
|
||||
|
||||
📜 List of closed issues is available [here](TODO)
|
||||
|
||||
---
|
||||
⏳ Please be patient, the builds are still [running](https://github.com/cryptomator/cryptomator/actions). New versions of Cryptomator can be found here in a few moments. ⏳
|
||||
|
||||
<!-- Don't forget to include the
|
||||
💾 SHA-256 checksums of release artifacts:
|
||||
@@ -78,4 +78,9 @@ jobs:
|
||||
```
|
||||
-->
|
||||
|
||||
As usual, the GPG signatures can be checked using [our public key `5811 7AFA 1F85 B3EE C154 677D 615D 449F E6E6 A235`](https://gist.github.com/cryptobot/211111cf092037490275f39d408f461a).
|
||||
> [!TIP]
|
||||
> You can verify the GPG signature of all assets using our public key: [`5811 7AFA 1F85 B3EE C154 677D 615D 449F E6E6 A235`](https://gist.github.com/cryptobot/211111cf092037490275f39d408f461a).
|
||||
|
||||
|
||||
|
||||
<!-- Auto-Generated Release Notes: -->
|
||||
|
||||
1
.github/workflows/mac-dmg-x64.yml
vendored
1
.github/workflows/mac-dmg-x64.yml
vendored
@@ -136,6 +136,7 @@ jobs:
|
||||
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\""
|
||||
--java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support/Cryptomator/mnt\""
|
||||
--java-options "-Dcryptomator.showTrayIcon=true"
|
||||
--java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism"
|
||||
--java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\""
|
||||
--mac-package-identifier org.cryptomator
|
||||
--resource-dir dist/mac/resources
|
||||
|
||||
1
.github/workflows/mac-dmg.yml
vendored
1
.github/workflows/mac-dmg.yml
vendored
@@ -134,6 +134,7 @@ jobs:
|
||||
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\""
|
||||
--java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support/Cryptomator/mnt\""
|
||||
--java-options "-Dcryptomator.showTrayIcon=true"
|
||||
--java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism"
|
||||
--java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\""
|
||||
--java-options "-XX:ErrorFile=/cryptomator/cryptomator_crash.log"
|
||||
--mac-package-identifier org.cryptomator
|
||||
|
||||
8
.idea/inspectionProfiles/Project_Default.xml
generated
8
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,10 +1,8 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="SpellCheckingInspection" enabled="true" level="TYPO" enabled_by_default="true">
|
||||
<option name="processCode" value="true" />
|
||||
<option name="processLiterals" value="true" />
|
||||
<option name="processComments" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="Deprecation" enabled="true" level="WARNING" enabled_by_default="true" editorAttributes="DEPRECATED_ATTRIBUTES" />
|
||||
<inspection_tool class="MarkedForRemoval" enabled="true" level="WARNING" enabled_by_default="true" editorAttributes="MARKED_FOR_REMOVAL_ATTRIBUTES" />
|
||||
<inspection_tool class="RedundantScheduledForRemovalAnnotation" enabled="true" level="WARNING" enabled_by_default="true" editorAttributes="MARKED_FOR_REMOVAL_ATTRIBUTES" />
|
||||
</profile>
|
||||
</component>
|
||||
2
.idea/runConfigurations/Cryptomator_macOS.xml
generated
2
.idea/runConfigurations/Cryptomator_macOS.xml
generated
@@ -5,7 +5,7 @@
|
||||
</envs>
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<module name="cryptomator" />
|
||||
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath="@{userhome}/Library/Application Support/Cryptomator/settings.json" -Dcryptomator.p12Path="@{userhome}/Library/Application Support/Cryptomator/key.p12" -Dcryptomator.ipcSocketPath="@{userhome}/Library/Application Support/Cryptomator/ipc.socket" -Dcryptomator.logDir="@{userhome}/Library/Logs/Cryptomator" -Dcryptomator.pluginDir="@{userhome}/Library/Application Support/Cryptomator/Plugins" -Dcryptomator.mountPointsDir="@{userhome}/Cryptomator" -Dcryptomator.showTrayIcon=true -Dcryptomator.integrationsMac.keychainServiceName=Cryptomator -Xss2m -Xmx512m -ea --enable-preview --enable-native-access=org.cryptomator.jfuse.mac,javafx.graphics" />
|
||||
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath="@{userhome}/Library/Application Support/Cryptomator/settings.json" -Dcryptomator.p12Path="@{userhome}/Library/Application Support/Cryptomator/key.p12" -Dcryptomator.ipcSocketPath="@{userhome}/Library/Application Support/Cryptomator/ipc.socket" -Dcryptomator.logDir="@{userhome}/Library/Logs/Cryptomator" -Dcryptomator.pluginDir="@{userhome}/Library/Application Support/Cryptomator/Plugins" -Dcryptomator.mountPointsDir="@{userhome}/Cryptomator" -Dcryptomator.showTrayIcon=true -Dcryptomator.integrationsMac.keychainServiceName=Cryptomator -Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism -Xss2m -Xmx512m -ea --enable-preview --enable-native-access=org.cryptomator.jfuse.mac,javafx.graphics" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
|
||||
19
CHANGELOG.md
Normal file
19
CHANGELOG.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
The changelog starts with version 1.19.0.
|
||||
Changes to prior versions can be found on the [Github release page](https://github.com/cryptomator/cryptomator/releases).
|
||||
|
||||
## [Unreleased](https://github.com/cryptomator/cryptomator/compare/1.18.0...HEAD)
|
||||
|
||||
### Added
|
||||
* New Self-Update Mechanism (#3948)
|
||||
* Implemented `.dmg` update mechanism
|
||||
* Implemented Flatpak update mechanism
|
||||
|
||||
### Changed
|
||||
* Built using JDK 25 (#4031)
|
||||
* Modernized Templage for GitHub Releases
|
||||
1
dist/mac/dmg/build.sh
vendored
1
dist/mac/dmg/build.sh
vendored
@@ -123,6 +123,7 @@ ${JAVA_HOME}/bin/jpackage \
|
||||
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"${APP_NAME}\"" \
|
||||
--java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support${APP_NAME}/mnt\"" \
|
||||
--java-options "-Dcryptomator.showTrayIcon=true" \
|
||||
--java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism" \
|
||||
--java-options "-Dcryptomator.buildNumber=\"dmg-${REVISION_NO}\"" \
|
||||
--mac-package-identifier ${PACKAGE_IDENTIFIER} \
|
||||
--resource-dir ../resources
|
||||
|
||||
22
pom.xml
22
pom.xml
@@ -33,11 +33,11 @@
|
||||
<nonModularGroupIds>org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents</nonModularGroupIds>
|
||||
|
||||
<!-- cryptomator dependencies -->
|
||||
<cryptomator.cryptofs.version>2.10.0-SNAPSHOT</cryptomator.cryptofs.version>
|
||||
<cryptomator.integrations.version>1.7.0</cryptomator.integrations.version>
|
||||
<cryptomator.cryptofs.version>2.9.0</cryptomator.cryptofs.version>
|
||||
<cryptomator.integrations.version>1.8.0-beta1</cryptomator.integrations.version>
|
||||
<cryptomator.integrations.win.version>1.5.1</cryptomator.integrations.win.version>
|
||||
<cryptomator.integrations.mac.version>1.4.1</cryptomator.integrations.mac.version>
|
||||
<cryptomator.integrations.linux.version>1.6.1</cryptomator.integrations.linux.version>
|
||||
<cryptomator.integrations.mac.version>1.5.0-beta1</cryptomator.integrations.mac.version>
|
||||
<cryptomator.integrations.linux.version>1.7.0-beta1</cryptomator.integrations.linux.version>
|
||||
<cryptomator.fuse.version>5.1.0</cryptomator.fuse.version>
|
||||
<cryptomator.webdav.version>3.0.0</cryptomator.webdav.version>
|
||||
|
||||
@@ -75,6 +75,20 @@
|
||||
<surefire.jacoco.args></surefire.jacoco.args>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<name>Central Portal Snapshots</name>
|
||||
<id>central-portal-snapshots</id>
|
||||
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<!-- Cryptomator Libs -->
|
||||
<dependency>
|
||||
|
||||
@@ -50,12 +50,12 @@ open module org.cryptomator.desktop {
|
||||
requires io.github.coffeelibs.tinyoauth2client;
|
||||
requires org.slf4j;
|
||||
requires org.apache.commons.lang3;
|
||||
requires com.github.benmanes.caffeine;
|
||||
|
||||
/* dagger bs */
|
||||
requires jakarta.inject;
|
||||
requires static javax.inject;
|
||||
requires java.compiler;
|
||||
requires com.github.benmanes.caffeine;
|
||||
|
||||
uses org.cryptomator.common.locationpresets.LocationPresetsProvider;
|
||||
uses SSLContextProvider;
|
||||
|
||||
@@ -74,13 +74,6 @@ public abstract class CommonsModule {
|
||||
return new MasterkeyFileAccess(Constants.PEPPER, csprng);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("SemVer")
|
||||
static Comparator<String> providesSemVerComparator() {
|
||||
return new SemVerComparator();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static Optional<RevealPathService> provideRevealPathService() {
|
||||
|
||||
@@ -124,6 +124,15 @@ public class Environment {
|
||||
return Optional.ofNullable(System.getProperty(BUILD_NUMBER_PROP_NAME));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the app version concatenated with the build number (if defined).
|
||||
*
|
||||
* @return version string formatted like {@code 1.2.3-4567} or {@code 1.2.3} if no build number is defined.
|
||||
*/
|
||||
public String getAppVersionWithBuildNumber() {
|
||||
return getAppVersion() + getBuildNumber().map("-"::concat).orElse("");
|
||||
}
|
||||
|
||||
public Optional<Path> getPluginDir() {
|
||||
return getPath(PLUGIN_DIR_PROP_NAME);
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
|
||||
* All rights reserved.
|
||||
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Compares version strings according to <a href="http://semver.org/spec/v2.0.0.html">SemVer 2.0.0</a>.
|
||||
*/
|
||||
public class SemVerComparator implements Comparator<String> {
|
||||
|
||||
private static final char VERSION_SEP = '.'; // http://semver.org/spec/v2.0.0.html#spec-item-2
|
||||
private static final String PRE_RELEASE_SEP = "-"; // http://semver.org/spec/v2.0.0.html#spec-item-9
|
||||
private static final String BUILD_SEP = "+"; // http://semver.org/spec/v2.0.0.html#spec-item-10
|
||||
|
||||
@Override
|
||||
public int compare(String version1, String version2) {
|
||||
// "Build metadata SHOULD be ignored when determining version precedence.
|
||||
// Thus two versions that differ only in the build metadata, have the same precedence."
|
||||
String v1WithoutBuildMetadata = StringUtils.substringBefore(version1, BUILD_SEP);
|
||||
String v2WithoutBuildMetadata = StringUtils.substringBefore(version2, BUILD_SEP);
|
||||
|
||||
if (v1WithoutBuildMetadata.equals(v2WithoutBuildMetadata)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
String v1MajorMinorPatch = StringUtils.substringBefore(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
String v2MajorMinorPatch = StringUtils.substringBefore(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
String v1PreReleaseVersion = StringUtils.substringAfter(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
String v2PreReleaseVersion = StringUtils.substringAfter(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
return compare(v1MajorMinorPatch, v1PreReleaseVersion, v2MajorMinorPatch, v2PreReleaseVersion);
|
||||
}
|
||||
|
||||
private int compare(String v1MajorMinorPatch, String v1PreReleaseVersion, String v2MajorMinorPatch, String v2PreReleaseVersion) {
|
||||
int comparisonResult = compareNumericallyThenLexicographically(v1MajorMinorPatch, v2MajorMinorPatch);
|
||||
if (comparisonResult == 0) {
|
||||
if (v1PreReleaseVersion.isEmpty()) {
|
||||
return 1; // 1.0.0 > 1.0.0-BETA
|
||||
} else if (v2PreReleaseVersion.isEmpty()) {
|
||||
return -1; // 1.0.0-BETA < 1.0.0
|
||||
} else {
|
||||
return compareNumericallyThenLexicographically(v1PreReleaseVersion, v2PreReleaseVersion);
|
||||
}
|
||||
} else {
|
||||
return comparisonResult;
|
||||
}
|
||||
}
|
||||
|
||||
private int compareNumericallyThenLexicographically(String version1, String version2) {
|
||||
final String[] vComps1 = StringUtils.split(version1, VERSION_SEP);
|
||||
final String[] vComps2 = StringUtils.split(version2, VERSION_SEP);
|
||||
final int commonCompCount = Math.min(vComps1.length, vComps2.length);
|
||||
|
||||
for (int i = 0; i < commonCompCount; i++) {
|
||||
int subversionComparisonResult = 0;
|
||||
try {
|
||||
final int v1 = Integer.parseInt(vComps1[i]);
|
||||
final int v2 = Integer.parseInt(vComps2[i]);
|
||||
subversionComparisonResult = v1 - v2;
|
||||
} catch (NumberFormatException ex) {
|
||||
// ok, lets compare this fragment lexicographically
|
||||
subversionComparisonResult = vComps1[i].compareTo(vComps2[i]);
|
||||
}
|
||||
if (subversionComparisonResult != 0) {
|
||||
return subversionComparisonResult;
|
||||
}
|
||||
}
|
||||
|
||||
// all in common so far? longest version string is considered the higher version:
|
||||
return vComps1.length - vComps2.length;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,10 +25,8 @@ import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.NodeOrientation;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class Settings {
|
||||
|
||||
@@ -53,6 +51,7 @@ public class Settings {
|
||||
static final String DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT.name();
|
||||
public static final Instant DEFAULT_TIMESTAMP = Instant.parse("2000-01-01T00:00:00Z");
|
||||
|
||||
private final SettingsProvider provider;
|
||||
public final ObservableList<VaultSettings> directories;
|
||||
public final BooleanProperty startHidden;
|
||||
public final BooleanProperty autoCloseVaults;
|
||||
@@ -78,13 +77,12 @@ public class Settings {
|
||||
public final ObjectProperty<Instant> lastUpdateCheckReminder;
|
||||
public final ObjectProperty<Instant> lastSuccessfulUpdateCheck;
|
||||
public final ObjectProperty<Path> previouslyUsedVaultDirectory;
|
||||
public final StringProperty lastUpdateAttemptedByVersion;
|
||||
|
||||
private Consumer<Settings> saveCmd;
|
||||
|
||||
public static Settings create(Environment env) {
|
||||
public static Settings create(SettingsProvider provider, Environment env) {
|
||||
var defaults = new SettingsJson();
|
||||
defaults.showTrayIcon = env.showTrayIcon();
|
||||
return new Settings(defaults);
|
||||
return new Settings(provider, defaults);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +90,8 @@ public class Settings {
|
||||
*
|
||||
* @param json The parsed settings.json
|
||||
*/
|
||||
Settings(SettingsJson json) {
|
||||
Settings(SettingsProvider provider, SettingsJson json) {
|
||||
this.provider = provider;
|
||||
this.directories = FXCollections.observableArrayList(VaultSettings::observables);
|
||||
this.startHidden = new SimpleBooleanProperty(this, "startHidden", json.startHidden);
|
||||
this.autoCloseVaults = new SimpleBooleanProperty(this, "autoCloseVaults", json.autoCloseVaults);
|
||||
@@ -118,6 +117,7 @@ public class Settings {
|
||||
this.lastUpdateCheckReminder = new SimpleObjectProperty<>(this, "lastUpdateCheckReminder", json.lastReminderForUpdateCheck);
|
||||
this.lastSuccessfulUpdateCheck = new SimpleObjectProperty<>(this, "lastSuccessfulUpdateCheck", json.lastSuccessfulUpdateCheck);
|
||||
this.previouslyUsedVaultDirectory = new SimpleObjectProperty<>(this, "previouslyUsedVaultDirectory", json.previouslyUsedVaultDirectory);
|
||||
this.lastUpdateAttemptedByVersion = new SimpleStringProperty(this, "lastUpdateAttemptedByVersion", json.lastUpdateAttemptedByVersion);
|
||||
|
||||
this.directories.addAll(json.directories.stream().map(VaultSettings::new).toList());
|
||||
|
||||
@@ -148,6 +148,7 @@ public class Settings {
|
||||
lastUpdateCheckReminder.addListener(this::somethingChanged);
|
||||
lastSuccessfulUpdateCheck.addListener(this::somethingChanged);
|
||||
previouslyUsedVaultDirectory.addListener(this::somethingChanged);
|
||||
lastUpdateAttemptedByVersion.addListener(this::somethingChanged);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@@ -210,6 +211,7 @@ public class Settings {
|
||||
json.lastReminderForUpdateCheck = lastUpdateCheckReminder.get();
|
||||
json.lastSuccessfulUpdateCheck = lastSuccessfulUpdateCheck.get();
|
||||
json.previouslyUsedVaultDirectory = previouslyUsedVaultDirectory.get();
|
||||
json.lastUpdateAttemptedByVersion = lastUpdateAttemptedByVersion.get();
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -222,20 +224,12 @@ public class Settings {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO rename to setChangeListener
|
||||
void setSaveCmd(Consumer<Settings> saveCmd) {
|
||||
this.saveCmd = saveCmd;
|
||||
}
|
||||
|
||||
private void somethingChanged(@SuppressWarnings("unused") Observable observable) {
|
||||
this.save();
|
||||
provider.scheduleSave(this);
|
||||
}
|
||||
|
||||
void save() {
|
||||
if (saveCmd != null) {
|
||||
saveCmd.accept(this);
|
||||
}
|
||||
public void saveNow() {
|
||||
provider.saveNow(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -96,4 +96,7 @@ class SettingsJson {
|
||||
|
||||
@JsonProperty("previouslyUsedVaultDirectory")
|
||||
Path previouslyUsedVaultDirectory;
|
||||
|
||||
@JsonProperty("lastUpdateAttemptedByVersion")
|
||||
String lastUpdateAttemptedByVersion;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,9 @@ import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -61,8 +63,7 @@ public class SettingsProvider implements Supplier<Settings> {
|
||||
Settings settings = env.getSettingsPath() //
|
||||
.flatMap(this::tryLoad) //
|
||||
.findFirst() //
|
||||
.orElseGet(() -> Settings.create(env));
|
||||
settings.setSaveCmd(this::scheduleSave);
|
||||
.orElseGet(() -> Settings.create(this, env));
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ public class SettingsProvider implements Supplier<Settings> {
|
||||
try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ)) {
|
||||
var json = JSON.reader().readValue(in, SettingsJson.class);
|
||||
LOG.info("Settings loaded from {}", path);
|
||||
var settings = new Settings(json);
|
||||
var settings = new Settings(this, json);
|
||||
return Stream.of(settings);
|
||||
} catch (JacksonException e) {
|
||||
LOG.warn("Failed to parse json file {}", path, e);
|
||||
@@ -84,19 +85,33 @@ public class SettingsProvider implements Supplier<Settings> {
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleSave(Settings settings) {
|
||||
if (settings == null) {
|
||||
return;
|
||||
void saveNow(Settings settings) {
|
||||
try {
|
||||
scheduleSave(settings, 0L).get();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LOG.error("Saving settings was interrupted.", e);
|
||||
} catch (ExecutionException e) {
|
||||
LOG.error("Unexpected exception while saving.", e);
|
||||
}
|
||||
final Optional<Path> settingsPath = env.getSettingsPath().findFirst(); // always save to preferred (first) path
|
||||
settingsPath.ifPresent(path -> {
|
||||
Runnable saveCommand = () -> this.save(settings, path);
|
||||
ScheduledFuture<?> scheduledTask = scheduler.schedule(saveCommand, SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
|
||||
ScheduledFuture<?> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask);
|
||||
if (previouslyScheduledTask != null) {
|
||||
previouslyScheduledTask.cancel(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void scheduleSave(Settings settings) {
|
||||
scheduleSave(settings, SAVE_DELAY_MS);
|
||||
}
|
||||
|
||||
private Future<?> scheduleSave(Settings settings, long delayMillis) {
|
||||
if (settings == null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
final Path settingsPath = env.getSettingsPath().findFirst().orElseThrow(); // always save to preferred (first) path
|
||||
Runnable saveCommand = () -> this.save(settings, settingsPath);
|
||||
ScheduledFuture<?> scheduledTask = scheduler.schedule(saveCommand, delayMillis, TimeUnit.MILLISECONDS);
|
||||
ScheduledFuture<?> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask);
|
||||
if (previouslyScheduledTask != null) {
|
||||
previouslyScheduledTask.cancel(false);
|
||||
}
|
||||
return scheduledTask;
|
||||
}
|
||||
|
||||
private void save(Settings settings, Path settingsPath) {
|
||||
@@ -107,7 +122,7 @@ public class SettingsProvider implements Supplier<Settings> {
|
||||
Path tmpPath = settingsPath.resolveSibling(settingsPath.getFileName().toString() + ".tmp");
|
||||
try (OutputStream out = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
|
||||
var jsonObj = settings.serialized();
|
||||
jsonObj.writtenByVersion = env.getAppVersion() + env.getBuildNumber().map("-"::concat).orElse("");
|
||||
jsonObj.writtenByVersion = env.getAppVersionWithBuildNumber();
|
||||
JSON.writerWithDefaultPrettyPrinter().writeValue(out, jsonObj);
|
||||
}
|
||||
Files.move(tmpPath, settingsPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
@@ -26,7 +26,7 @@ import javafx.scene.image.Image;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {TrayMenuComponent.class, //
|
||||
@Module(subcomponents = {TrayMenuComponent.class, //
|
||||
DecryptNameComponent.class, //
|
||||
MainWindowComponent.class, //
|
||||
PreferencesComponent.class, //
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
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 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 AUTO_CHECK_DELAY = Duration.seconds(5);
|
||||
|
||||
private final Environment env;
|
||||
private final Settings settings;
|
||||
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, //
|
||||
ScheduledService<String> updateCheckerService) {
|
||||
this.env = env;
|
||||
this.settings = settings;
|
||||
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(AUTO_CHECK_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
public void checkForUpdatesNow() {
|
||||
startCheckingForUpdates(Duration.ZERO);
|
||||
}
|
||||
|
||||
private void startCheckingForUpdates(Duration initialDelay) {
|
||||
updateCheckerService.cancel();
|
||||
updateCheckerService.reset();
|
||||
updateCheckerService.setDelay(initialDelay);
|
||||
updateCheckerService.setOnRunning(this::checkStarted);
|
||||
updateCheckerService.setOnSucceeded(this::checkSucceeded);
|
||||
updateCheckerService.setOnFailed(this::checkFailed);
|
||||
updateCheckerService.start();
|
||||
}
|
||||
|
||||
private void checkStarted(WorkerStateEvent event) {
|
||||
LOG.debug("Checking for updates...");
|
||||
state.set(UpdateCheckState.IS_CHECKING);
|
||||
}
|
||||
|
||||
private void checkSucceeded(WorkerStateEvent event) {
|
||||
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) {
|
||||
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 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();
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package org.cryptomator.ui.fxapp;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.util.Duration;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@Module
|
||||
public abstract class UpdateCheckerModule {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateCheckerModule.class);
|
||||
|
||||
private static final URI LATEST_VERSION_URI = URI.create("https://api.cryptomator.org/desktop/latest-version.json");
|
||||
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
|
||||
@FxApplicationScoped
|
||||
static Optional<HttpClient> provideHttpClient() {
|
||||
try {
|
||||
return Optional.of(HttpClient.newBuilder() //
|
||||
.followRedirects(HttpClient.Redirect.NORMAL) // from version 1.6.11 onwards, Cryptomator can follow redirects, in case this URL ever changes
|
||||
.build());
|
||||
} catch (UncheckedIOException e) {
|
||||
LOG.error("HttpClient for update check cannot be created.", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxApplicationScoped
|
||||
static HttpRequest provideCheckForUpdatesRequest(Environment env) {
|
||||
String userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", //
|
||||
env.getAppVersion(), //
|
||||
SystemUtils.OS_NAME, //
|
||||
SystemUtils.OS_VERSION, //
|
||||
SystemUtils.OS_ARCH); //
|
||||
return HttpRequest.newBuilder() //
|
||||
.uri(LATEST_VERSION_URI) //
|
||||
.header("User-Agent", userAgent) //
|
||||
.timeout(java.time.Duration.ofSeconds(10))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("checkForUpdatesInterval")
|
||||
@FxApplicationScoped
|
||||
static ObjectBinding<Duration> provideCheckForUpdateInterval(Settings settings) {
|
||||
return Bindings.when(settings.checkForUpdates).then(UPDATE_CHECK_INTERVAL).otherwise(DISABLED_UPDATE_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxApplicationScoped
|
||||
static ScheduledService<String> provideCheckForUpdatesService(ExecutorService executor, Optional<HttpClient> httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding<Duration> period) {
|
||||
ScheduledService<String> service = new ScheduledService<>() {
|
||||
@Override
|
||||
protected Task<String> createTask() {
|
||||
if (httpClient.isPresent()) {
|
||||
return new UpdateCheckerTask(httpClient.get(), checkForUpdatesRequest);
|
||||
} else {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected String call() {
|
||||
throw new NullPointerException("No HttpClient present.");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
service.setOnFailed(event -> LOG.error("Failed to execute update service", service.getException()));
|
||||
service.setExecutor(executor);
|
||||
service.periodProperty().bind(period);
|
||||
return service;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package org.cryptomator.ui.fxapp;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javafx.concurrent.Task;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
|
||||
public class UpdateCheckerTask extends Task<String> {
|
||||
|
||||
private static final ObjectMapper JSON = new ObjectMapper();
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateCheckerTask.class);
|
||||
|
||||
private static final long MAX_RESPONSE_SIZE = 10L * 1024; // 10kb should be sufficient. protect against flooding
|
||||
|
||||
private final HttpClient httpClient;
|
||||
private final HttpRequest checkForUpdatesRequest;
|
||||
|
||||
UpdateCheckerTask(HttpClient httpClient, HttpRequest checkForUpdatesRequest) {
|
||||
this.httpClient = httpClient;
|
||||
this.checkForUpdatesRequest = checkForUpdatesRequest;
|
||||
|
||||
setOnFailed(event -> LOG.error("Failed to check for updates", getException()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String call() throws IOException, InterruptedException {
|
||||
HttpResponse<InputStream> response = httpClient.send(checkForUpdatesRequest, HttpResponse.BodyHandlers.ofInputStream());
|
||||
if (response.statusCode() == 200) {
|
||||
return processBody(response);
|
||||
} else {
|
||||
throw new IOException("Unexpected HTTP response code " + response.statusCode());
|
||||
}
|
||||
}
|
||||
|
||||
private String processBody(HttpResponse<InputStream> response) throws IOException {
|
||||
try (InputStream in = response.body(); //
|
||||
InputStream limitedIn = ByteStreams.limit(in, MAX_RESPONSE_SIZE)) {
|
||||
var json = JSON.reader().readTree(limitedIn);
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
return json.get("mac").asText();
|
||||
} else if (SystemUtils.IS_OS_WINDOWS) {
|
||||
return json.get("win").asText();
|
||||
} else if (SystemUtils.IS_OS_LINUX) {
|
||||
return json.get("linux").asText();
|
||||
} else {
|
||||
throw new IllegalStateException("Unsupported operating system");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
||||
import org.cryptomator.updater.UpdateChecker;
|
||||
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.cryptomator.ui.preferences;
|
||||
import com.google.common.io.CharStreams;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
||||
import org.cryptomator.updater.UpdateChecker;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.cryptomator.ui.preferences;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
||||
import org.cryptomator.updater.UpdateChecker;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
||||
@@ -2,25 +2,37 @@ package org.cryptomator.ui.preferences;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.integrations.update.UpdateStep;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
||||
import org.cryptomator.ui.common.VaultService;
|
||||
import org.cryptomator.updater.UpdateChecker;
|
||||
import org.cryptomator.updater.FallbackUpdateInfo;
|
||||
import org.cryptomator.updater.UpdateService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.BooleanExpression;
|
||||
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.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.concurrent.WorkerStateEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -34,73 +46,64 @@ import java.util.ResourceBundle;
|
||||
@PreferencesScoped
|
||||
public class UpdatesPreferencesController implements FxController {
|
||||
|
||||
private static final String DOWNLOADS_URI_TEMPLATE = "https://cryptomator.org/downloads/" //
|
||||
+ "?utm_source=cryptomator-desktop" //
|
||||
+ "&utm_medium=update-notification&" //
|
||||
+ "utm_campaign=app-update-%s";
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdatesPreferencesController.class);
|
||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
|
||||
|
||||
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 UpdateService updateService;
|
||||
private final ObservableList<Vault> unlockedVaults;
|
||||
private final VaultService vaultService;
|
||||
private final ObjectBinding<Worker<?>> worker;
|
||||
private final BooleanExpression running;
|
||||
private final StringBinding updateButtonTitle;
|
||||
private final ObjectBinding<ContentDisplay> updateButtonState;
|
||||
private final ObservableValue<String> timeDifferenceMessage;
|
||||
private final String currentVersion;
|
||||
private final BooleanBinding updateAvailable;
|
||||
private final BooleanBinding checkFailed;
|
||||
private final StringBinding lastUpdateCheckMessage;
|
||||
private final BooleanBinding prohibitUpdateWhileUnlocked;
|
||||
private final BooleanBinding updateButtonDisabled;
|
||||
private final StringProperty errorMessage = new SimpleStringProperty("");
|
||||
private final BooleanProperty upToDateLabelVisible = new SimpleBooleanProperty(false);
|
||||
private final DateTimeFormatter formatter;
|
||||
private final BooleanBinding upToDate;
|
||||
private final String downloadsUri;
|
||||
|
||||
/* FXML */
|
||||
public CheckBox checkForUpdatesCheckbox;
|
||||
|
||||
@Inject
|
||||
UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker) {
|
||||
UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker, ObservableList<Vault> vaults, VaultService vaultService) {
|
||||
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.lastSuccessfulUpdateCheck = updateChecker.lastSuccessfulUpdateCheckProperty();
|
||||
this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, lastSuccessfulUpdateCheck);
|
||||
this.currentVersion = environment.getAppVersion();
|
||||
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);
|
||||
this.downloadsUri = DOWNLOADS_URI_TEMPLATE.formatted(URLEncoder.encode(currentVersion, StandardCharsets.US_ASCII));
|
||||
this.updateService = new UpdateService(updateChecker.updateProperty());
|
||||
this.unlockedVaults = vaults.filtered(Vault::isUnlocked);
|
||||
this.vaultService = vaultService;
|
||||
this.worker = Bindings.when(updateChecker.updateAvailableProperty()).<Worker<?>>then(this.updateService).otherwise(this.updateChecker);
|
||||
this.running = Bindings.createBooleanBinding(this::isRunning, updateService.stateProperty(), updateChecker.stateProperty());
|
||||
this.updateButtonTitle = Bindings.createStringBinding(this::getUpdateButtonTitle, worker, updateService.stateProperty(), updateService.messageProperty());
|
||||
this.updateButtonState = Bindings.createObjectBinding(this::getUpdateButtonState, updateChecker.stateProperty(), updateService.stateProperty());
|
||||
this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, updateChecker.lastSuccessfulUpdateCheckProperty());
|
||||
this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, updateChecker.lastSuccessfulUpdateCheckProperty());
|
||||
this.prohibitUpdateWhileUnlocked = Bindings.createBooleanBinding(this::isProhibitUpdateWhileUnlocked, unlockedVaults, updateChecker.updateProperty());
|
||||
this.updateButtonDisabled = Bindings.when(worker.isEqualTo(updateChecker)).then(running).otherwise(prohibitUpdateWhileUnlocked.or(running));
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
checkForUpdatesCheckbox.selectedProperty().bindBidirectional(settings.checkForUpdates);
|
||||
|
||||
upToDate.addListener((_, _, newVal) -> {
|
||||
if (newVal) {
|
||||
updateChecker.updateAvailableProperty().addListener((_, _, hasUpdate) -> {
|
||||
if (!hasUpdate) {
|
||||
upToDateLabelVisible.set(true);
|
||||
PauseTransition delay = new PauseTransition(javafx.util.Duration.seconds(5));
|
||||
delay.setOnFinished(_ -> upToDateLabelVisible.set(false));
|
||||
delay.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void checkNow() {
|
||||
updateChecker.checkForUpdatesNow();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void visitDownloadsPage() {
|
||||
application.getHostServices().showDocument(downloadsUri);
|
||||
updateChecker.setOnFailed(this::checkFailed);
|
||||
updateService.setOnSucceeded(this::updateSucceeded);
|
||||
updateService.setOnFailed(this::updateFailed);
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -108,38 +111,104 @@ public class UpdatesPreferencesController implements FxController {
|
||||
environment.getLogDir().ifPresent(logDirPath -> application.getHostServices().showDocument(logDirPath.toUri().toString()));
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void startWork() {
|
||||
if (worker.get().equals(updateChecker)) {
|
||||
updateChecker.checkForUpdatesNow();
|
||||
} else if (!unlockedVaults.isEmpty()) {
|
||||
LOG.warn("Cannot start update due to unlocked vaults.");
|
||||
} else if (worker.get().equals(updateService)) {
|
||||
LOG.info("User started update to version {}", updateChecker.getUpdate().version());
|
||||
updateService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkFailed(WorkerStateEvent workerStateEvent) {
|
||||
assert workerStateEvent.getSource() == updateChecker;
|
||||
LOG.error("Update check failed.", updateChecker.getException());
|
||||
errorMessage.set(resourceBundle.getString("preferences.updates.checkFailed"));
|
||||
}
|
||||
|
||||
private void updateSucceeded(WorkerStateEvent workerStateEvent) {
|
||||
assert workerStateEvent.getSource() == updateService;
|
||||
var lastStep = updateService.getValue();
|
||||
if (lastStep == UpdateStep.EXIT) {
|
||||
// Record that this version attempted an update, so next launch can choose fallback if needed
|
||||
settings.lastUpdateAttemptedByVersion.set(environment.getAppVersionWithBuildNumber());
|
||||
settings.saveNow();
|
||||
LOG.info("Exiting app to update...");
|
||||
Platform.exit();
|
||||
} else if (lastStep == UpdateStep.RETRY) {
|
||||
updateService.reset();
|
||||
} else {
|
||||
LOG.info("Update succeeded.");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFailed(WorkerStateEvent workerStateEvent) {
|
||||
assert workerStateEvent.getSource() == updateService;
|
||||
LOG.error("Update failed.", updateService.getException());
|
||||
updateService.reset();
|
||||
errorMessage.set(resourceBundle.getString("preferences.updates.updateFailed"));
|
||||
// try fallback mechanism:
|
||||
updateChecker.recheckWithFallbackMechanism();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void lockAllGracefully() {
|
||||
vaultService.lockAll(unlockedVaults, false);
|
||||
}
|
||||
|
||||
/* Observable Properties */
|
||||
|
||||
public ObjectBinding<ContentDisplay> checkForUpdatesButtonStateProperty() {
|
||||
return checkForUpdatesButtonState;
|
||||
public UpdateChecker getUpdateChecker() {
|
||||
return updateChecker;
|
||||
}
|
||||
|
||||
public ContentDisplay getCheckForUpdatesButtonState() {
|
||||
return checkForUpdatesButtonState.get();
|
||||
public ObjectBinding<Worker<?>> workerProperty() {
|
||||
return worker;
|
||||
}
|
||||
|
||||
public ReadOnlyStringProperty latestVersionProperty() {
|
||||
return latestVersion;
|
||||
public Worker<?> getWorker() {
|
||||
return worker.get();
|
||||
}
|
||||
|
||||
public String getLatestVersion() {
|
||||
return latestVersion.get();
|
||||
public BooleanExpression runningProperty() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
return currentVersion;
|
||||
public boolean isRunning() {
|
||||
return updateChecker.getState() == Worker.State.RUNNING || updateService.getState() == Worker.State.RUNNING;
|
||||
}
|
||||
|
||||
public StringBinding lastUpdateCheckMessageProperty() {
|
||||
return lastUpdateCheckMessage;
|
||||
public StringBinding updateButtonTitleProperty() {
|
||||
return updateButtonTitle;
|
||||
}
|
||||
|
||||
public String getLastUpdateCheckMessage() {
|
||||
Instant lastCheck = lastSuccessfulUpdateCheck.getValue();
|
||||
if (lastCheck != null && !lastCheck.equals(Settings.DEFAULT_TIMESTAMP)) {
|
||||
return formatter.format(LocalDateTime.ofInstant(lastCheck, ZoneId.systemDefault()));
|
||||
public String getUpdateButtonTitle() {
|
||||
if (worker.get() == updateChecker) {
|
||||
return resourceBundle.getString("preferences.updates.checkNowBtn");
|
||||
} else {
|
||||
return "-";
|
||||
return switch (updateService.getState()) {
|
||||
case READY -> updateChecker.getUpdate().updateMechanism().getName();
|
||||
case SCHEDULED, RUNNING -> updateService.getMessage();
|
||||
case SUCCEEDED -> resourceBundle.getString("generic.button.done");
|
||||
case FAILED, CANCELLED -> "failed"; // should never be visible
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public ObjectBinding<ContentDisplay> updateButtonStateProperty() {
|
||||
return updateButtonState;
|
||||
}
|
||||
|
||||
public ContentDisplay getUpdateButtonState() {
|
||||
if (updateService.isRunning()) { // isRunning() covers RUNNING and SCHEDULED states
|
||||
return ContentDisplay.BOTTOM;
|
||||
} else if (updateChecker.getState() == Worker.State.RUNNING) {
|
||||
return ContentDisplay.LEFT;
|
||||
} else {
|
||||
return ContentDisplay.TEXT_ONLY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +217,7 @@ public class UpdatesPreferencesController implements FxController {
|
||||
}
|
||||
|
||||
public String getTimeDifferenceMessage() {
|
||||
var lastSuccessCheck = lastSuccessfulUpdateCheck.getValue();
|
||||
var lastSuccessCheck = updateChecker.getLastSuccessfulUpdateCheck();
|
||||
var duration = Duration.between(lastSuccessCheck, Instant.now());
|
||||
var hours = duration.toHours();
|
||||
if (lastSuccessCheck.equals(Settings.DEFAULT_TIMESTAMP)) {
|
||||
@@ -162,6 +231,44 @@ public class UpdatesPreferencesController implements FxController {
|
||||
}
|
||||
}
|
||||
|
||||
public StringBinding lastUpdateCheckMessageProperty() {
|
||||
return lastUpdateCheckMessage;
|
||||
}
|
||||
|
||||
public String getLastUpdateCheckMessage() {
|
||||
Instant lastCheck = updateChecker.getLastSuccessfulUpdateCheck();
|
||||
if (lastCheck != null && !lastCheck.equals(Settings.DEFAULT_TIMESTAMP)) {
|
||||
return FORMATTER.format(LocalDateTime.ofInstant(lastCheck, ZoneId.systemDefault()));
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage.get();
|
||||
}
|
||||
|
||||
public ReadOnlyStringProperty errorMessageProperty() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public boolean isProhibitUpdateWhileUnlocked() {
|
||||
// If the result of the last update check was from the fallback mechanism, we don't need to show the warning
|
||||
return !unlockedVaults.isEmpty() && !FallbackUpdateInfo.class.isInstance(updateChecker.getUpdate());
|
||||
}
|
||||
|
||||
public BooleanBinding prohibitUpdateWhileUnlockedProperty() {
|
||||
return prohibitUpdateWhileUnlocked;
|
||||
}
|
||||
|
||||
public boolean isUpdateButtonDisabled() {
|
||||
return updateButtonDisabled.get();
|
||||
}
|
||||
|
||||
public BooleanBinding updateButtonDisabledProperty() {
|
||||
return updateButtonDisabled;
|
||||
}
|
||||
|
||||
public BooleanProperty upToDateLabelVisibleProperty() {
|
||||
return upToDateLabelVisible;
|
||||
}
|
||||
@@ -170,20 +277,4 @@ public class UpdatesPreferencesController implements FxController {
|
||||
return upToDateLabelVisible.get();
|
||||
}
|
||||
|
||||
public BooleanBinding updateAvailableProperty() {
|
||||
return updateAvailable;
|
||||
}
|
||||
|
||||
public boolean isUpdateAvailable() {
|
||||
return updateAvailable.get();
|
||||
}
|
||||
|
||||
public BooleanBinding checkFailedProperty() {
|
||||
return checkFailed;
|
||||
}
|
||||
|
||||
public boolean isCheckFailed() {
|
||||
return checkFailed.getValue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.cryptomator.ui.updatereminder;
|
||||
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
||||
import org.cryptomator.updater.UpdateChecker;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.fxml.FXML;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package org.cryptomator.updater;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import java.io.IOException;
|
||||
import java.net.Authenticator;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
abstract class DelegatingHttpClient extends HttpClient {
|
||||
|
||||
private final HttpClient delegate;
|
||||
|
||||
public DelegatingHttpClient(HttpClient delegate) {
|
||||
this.delegate = Objects.requireNonNull(delegate, "delegate must not be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<CookieHandler> cookieHandler() {
|
||||
return delegate.cookieHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Duration> connectTimeout() {
|
||||
return delegate.connectTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Redirect followRedirects() {
|
||||
return delegate.followRedirects();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProxySelector> proxy() {
|
||||
return delegate.proxy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSLContext sslContext() {
|
||||
return delegate.sslContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSLParameters sslParameters() {
|
||||
return delegate.sslParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Authenticator> authenticator() {
|
||||
return delegate.authenticator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version version() {
|
||||
return delegate.version();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Executor> executor() {
|
||||
return delegate.executor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException, InterruptedException {
|
||||
return delegate.send(request, responseBodyHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) {
|
||||
return delegate.sendAsync(request, responseBodyHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, HttpResponse.PushPromiseHandler<T> pushPromiseHandler) {
|
||||
return delegate.sendAsync(request, responseBodyHandler, pushPromiseHandler);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.cryptomator.updater;
|
||||
|
||||
import org.cryptomator.integrations.update.UpdateInfo;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
|
||||
public record FallbackUpdateInfo(String version, UpdateMechanism<FallbackUpdateInfo> updateMechanism) implements UpdateInfo<FallbackUpdateInfo> {}
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.cryptomator.updater;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.integrations.common.LocalizedDisplayName;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
import org.cryptomator.integrations.update.UpdateStep;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@FxApplicationScoped
|
||||
@LocalizedDisplayName(bundle = "i18n.strings", key = "preferences.updates.visitDownloadPage")
|
||||
public class FallbackUpdateMechanism implements UpdateMechanism<FallbackUpdateInfo> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FallbackUpdateMechanism.class);
|
||||
private static final String LATEST_VERSION_API_URL = "https://api.cryptomator.org/connect/apps/desktop/latest-version";
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
private static final String DOWNLOADS_URI_TEMPLATE = "https://cryptomator.org/downloads/" //
|
||||
+ "?utm_source=cryptomator-desktop" //
|
||||
+ "&utm_medium=update-notification&" //
|
||||
+ "utm_campaign=app-update-%s";
|
||||
|
||||
private final Application app;
|
||||
private final Environment env;
|
||||
|
||||
@Inject
|
||||
public FallbackUpdateMechanism(Application app, Environment env) {
|
||||
this.app = app;
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FallbackUpdateInfo checkForUpdate(String currentVersion, HttpClient httpClient) {
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(LATEST_VERSION_API_URL)).build();
|
||||
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
||||
if (response.statusCode() != 200) {
|
||||
throw new RuntimeException("Failed to fetch release: " + response.statusCode());
|
||||
}
|
||||
var release = MAPPER.readValue(response.body(), LatestVersion.class);
|
||||
var updateVersion = release.versionForCurrentOS();
|
||||
if (UpdateMechanism.isUpdateAvailable(updateVersion, currentVersion)) {
|
||||
return new FallbackUpdateInfo(updateVersion, this);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LOG.warn("Update check interrupted", e);
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Update check failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateStep firstStep(FallbackUpdateInfo updateInfo) {
|
||||
return UpdateStep.of("Go to download page", this::openDownloadPage); // TODO localize
|
||||
}
|
||||
|
||||
private UpdateStep openDownloadPage() {
|
||||
var downloadUrl = DOWNLOADS_URI_TEMPLATE.formatted(URLEncoder.encode(env.getAppVersion(), StandardCharsets.US_ASCII));
|
||||
Platform.runLater(() -> {
|
||||
app.getHostServices().showDocument(downloadUrl);
|
||||
});
|
||||
return UpdateStep.RETRY; // allow running this "update mechanism" as many times as the user wants
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record LatestVersion(
|
||||
@JsonProperty("mac") String macVersion,
|
||||
@JsonProperty("win") String winVersion,
|
||||
@JsonProperty("linux") String linuxVersion
|
||||
) {
|
||||
public String versionForCurrentOS() {
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
return macVersion;
|
||||
} else if (SystemUtils.IS_OS_WINDOWS) {
|
||||
return winVersion;
|
||||
} else if (SystemUtils.IS_OS_LINUX) {
|
||||
return linuxVersion;
|
||||
} else {
|
||||
throw new IllegalStateException("Unsupported operating system");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
198
src/main/java/org/cryptomator/updater/UpdateChecker.java
Normal file
198
src/main/java/org/cryptomator/updater/UpdateChecker.java
Normal file
@@ -0,0 +1,198 @@
|
||||
package org.cryptomator.updater;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.integrations.update.UpdateFailedException;
|
||||
import org.cryptomator.integrations.update.UpdateInfo;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.StringExpression;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.util.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@FxApplicationScoped
|
||||
public class UpdateChecker extends ScheduledService<UpdateInfo<?>> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateChecker.class);
|
||||
private static final Duration AUTO_CHECK_DELAY = Duration.seconds(5);
|
||||
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...
|
||||
|
||||
public enum UpdateCheckState {
|
||||
NOT_CHECKED,
|
||||
IS_CHECKING,
|
||||
CHECK_SUCCESSFUL,
|
||||
CHECK_FAILED
|
||||
}
|
||||
|
||||
private final Environment env;
|
||||
private final Settings settings;
|
||||
private final ObjectProperty<Instant> lastSuccessfulUpdateCheck;
|
||||
private final ObjectProperty<UpdateInfo<?>> update = new SimpleObjectProperty<>();
|
||||
private final StringExpression latestVersion = StringExpression.stringExpression(update.map(UpdateInfo::version));
|
||||
private final BooleanBinding updateAvailable = update.isNotNull();
|
||||
private final ObjectBinding<UpdateCheckState> updateState = Bindings.createObjectBinding(this::getUpdateCheckState, stateProperty());
|
||||
private final BooleanBinding checkFailed = Bindings.equal(UpdateCheckState.CHECK_FAILED, updateState);
|
||||
private final UpdateMechanism<?> fallbackUpdateMechanism;
|
||||
private UpdateMechanism<?> updateMechanism;
|
||||
|
||||
@Inject
|
||||
UpdateChecker(Settings settings, //
|
||||
Environment env,
|
||||
FallbackUpdateMechanism fallbackUpdateMechanism) {
|
||||
this.env = env;
|
||||
this.settings = settings;
|
||||
this.lastSuccessfulUpdateCheck = settings.lastSuccessfulUpdateCheck;
|
||||
this.fallbackUpdateMechanism = fallbackUpdateMechanism;
|
||||
|
||||
// Prefer the safer fallback mechanism if the last update attempt was already made by this app version
|
||||
var currentVersion = env.getAppVersionWithBuildNumber();
|
||||
var lastAttemptedBy = settings.lastUpdateAttemptedByVersion.get();
|
||||
if (currentVersion != null && currentVersion.equals(lastAttemptedBy)) {
|
||||
this.updateMechanism = fallbackUpdateMechanism; // immediately use fallback mechanism
|
||||
} else {
|
||||
this.updateMechanism = UpdateMechanism.get().orElse(fallbackUpdateMechanism);
|
||||
}
|
||||
|
||||
setExecutor(Executors.newVirtualThreadPerTaskExecutor());
|
||||
periodProperty().bind(Bindings.when(settings.checkForUpdates).then(UPDATE_CHECK_INTERVAL).otherwise(DISABLED_UPDATE_CHECK_INTERVAL));
|
||||
}
|
||||
|
||||
public void automaticallyCheckForUpdatesIfEnabled() {
|
||||
if (!env.disableUpdateCheck() && settings.checkForUpdates.get()) {
|
||||
startCheckingForUpdates(AUTO_CHECK_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
public void recheckWithFallbackMechanism() {
|
||||
if (updateMechanism == fallbackUpdateMechanism) {
|
||||
return; // already using fallback mechanism
|
||||
}
|
||||
updateMechanism = fallbackUpdateMechanism;
|
||||
checkForUpdatesNow();
|
||||
}
|
||||
|
||||
public void checkForUpdatesNow() {
|
||||
startCheckingForUpdates(Duration.ZERO);
|
||||
}
|
||||
|
||||
private void startCheckingForUpdates(Duration initialDelay) {
|
||||
cancel();
|
||||
reset();
|
||||
setDelay(initialDelay);
|
||||
start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
var updateInfo = getValue();
|
||||
super.succeeded(); // this will nil the value property!
|
||||
lastSuccessfulUpdateCheck.set(Instant.now());
|
||||
if (updateInfo != null) {
|
||||
LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), updateInfo.version());
|
||||
update.set(updateInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<UpdateInfo<?>> createTask() {
|
||||
return new UpdateCheckTask();
|
||||
}
|
||||
|
||||
/* Observable Properties */
|
||||
|
||||
public UpdateInfo<?> getUpdate() {
|
||||
return update.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<UpdateInfo<?>> updateProperty() {
|
||||
return update;
|
||||
}
|
||||
|
||||
public String getLatestVersion() {
|
||||
return latestVersion.get();
|
||||
}
|
||||
|
||||
public StringExpression latestVersionProperty() {
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
public boolean isUpdateAvailable() {
|
||||
return updateAvailable.get();
|
||||
}
|
||||
|
||||
public BooleanBinding updateAvailableProperty() {
|
||||
return updateAvailable;
|
||||
}
|
||||
|
||||
public boolean isCheckFailed() {
|
||||
return checkFailed.get();
|
||||
}
|
||||
|
||||
public BooleanBinding checkFailedProperty() {
|
||||
return checkFailed;
|
||||
}
|
||||
|
||||
public Instant getLastSuccessfulUpdateCheck() {
|
||||
return lastSuccessfulUpdateCheck.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Instant> lastSuccessfulUpdateCheckProperty() {
|
||||
return lastSuccessfulUpdateCheck;
|
||||
}
|
||||
|
||||
public ObjectBinding<UpdateCheckState> updateCheckStateProperty() {
|
||||
return updateState;
|
||||
}
|
||||
|
||||
private UpdateCheckState getUpdateCheckState() {
|
||||
return switch (getState()) {
|
||||
case READY -> UpdateCheckState.NOT_CHECKED;
|
||||
case SCHEDULED, RUNNING -> UpdateCheckState.IS_CHECKING;
|
||||
case SUCCEEDED -> UpdateCheckState.CHECK_SUCCESSFUL;
|
||||
case FAILED, CANCELLED -> UpdateCheckState.CHECK_FAILED;
|
||||
};
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
return env.getAppVersion();
|
||||
}
|
||||
|
||||
private class UpdateCheckTask extends Task<UpdateInfo<?>> {
|
||||
|
||||
@Override
|
||||
protected UpdateInfo<?> call() {
|
||||
try (var httpClient = new UpdateCheckerHttpClient(env)) {
|
||||
var result = updateMechanism.checkForUpdate(env.getAppVersion(), httpClient);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
} catch (UpdateFailedException e) {
|
||||
LOG.error("Update check using {} failed.", updateMechanism.getClass(), e);
|
||||
}
|
||||
if (updateMechanism == fallbackUpdateMechanism) {
|
||||
return null;
|
||||
}
|
||||
LOG.debug("Trying fallback update check...");
|
||||
try (var httpClient = new UpdateCheckerHttpClient(env)) {
|
||||
return fallbackUpdateMechanism.checkForUpdate(env.getAppVersion(), httpClient);
|
||||
} catch (UpdateFailedException e) {
|
||||
LOG.error("Fallback update check failed.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.cryptomator.updater;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class UpdateCheckerHttpClient extends DelegatingHttpClient {
|
||||
|
||||
private final String userAgent;
|
||||
|
||||
public UpdateCheckerHttpClient(Environment env) {
|
||||
var delegate = HttpClient.newBuilder() //
|
||||
.followRedirects(HttpClient.Redirect.NORMAL) // from version 1.6.11 onwards, Cryptomator can follow redirects, in case this URL ever changes
|
||||
.proxy(ProxySelector.getDefault()).build();
|
||||
super(delegate);
|
||||
this.userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", env.getAppVersion(), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException, InterruptedException {
|
||||
return super.send(decorateRequest(request), responseBodyHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) {
|
||||
return super.sendAsync(decorateRequest(request), responseBodyHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, HttpResponse.PushPromiseHandler<T> pushPromiseHandler) {
|
||||
return super.sendAsync(decorateRequest(request), responseBodyHandler, pushPromiseHandler);
|
||||
}
|
||||
|
||||
private HttpRequest decorateRequest(HttpRequest request) {
|
||||
return HttpRequest.newBuilder(request, (_, _) -> true) //
|
||||
.header("User-Agent", this.userAgent) //
|
||||
.timeout(Duration.ofSeconds(10)) //
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
81
src/main/java/org/cryptomator/updater/UpdateService.java
Normal file
81
src/main/java/org/cryptomator/updater/UpdateService.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package org.cryptomator.updater;
|
||||
|
||||
import org.cryptomator.integrations.update.UpdateInfo;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
import org.cryptomator.integrations.update.UpdateStep;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* A service that performs all update steps provided by the given {@link UpdateMechanism} in sequence.
|
||||
*/
|
||||
public class UpdateService extends Service<UpdateStep> {
|
||||
|
||||
private final BooleanBinding updateFailed = Bindings.equal(State.FAILED, stateProperty());
|
||||
|
||||
private ObservableValue<UpdateInfo<?>> updateInfo;
|
||||
|
||||
public UpdateService(ObservableValue<UpdateInfo<?>> updateInfo) {
|
||||
setExecutor(Executors.newVirtualThreadPerTaskExecutor());
|
||||
this.updateInfo = updateInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<UpdateStep> createTask() {
|
||||
return new RunAllStepsTask(updateInfo.getValue());
|
||||
}
|
||||
|
||||
private static class RunAllStepsTask extends Task<UpdateStep> {
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private final UpdateInfo updateInfo;
|
||||
|
||||
public RunAllStepsTask(UpdateInfo<?> updateInfo) {
|
||||
this.updateInfo = Objects.requireNonNull(updateInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UpdateStep call() throws IOException {
|
||||
try {
|
||||
UpdateStep step = updateInfo.useToPrepareFirstStep();
|
||||
UpdateStep lastStep;
|
||||
do {
|
||||
step.start();
|
||||
observeAndWaitFor(step);
|
||||
lastStep = step;
|
||||
step = step.nextStep();
|
||||
} while (step != null);
|
||||
return lastStep;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new InterruptedIOException("Update interrupted");
|
||||
}
|
||||
}
|
||||
|
||||
private void observeAndWaitFor(UpdateStep step) throws InterruptedException {
|
||||
do {
|
||||
updateProgress(step.preparationProgress(), 1.0);
|
||||
updateMessage(step.description());
|
||||
} while (!step.await(100, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
/* Observable Properties */
|
||||
|
||||
public boolean isUpdateFailed() {
|
||||
return updateFailed.get();
|
||||
}
|
||||
|
||||
public BooleanBinding updateFailedProperty() {
|
||||
return updateFailed;
|
||||
}
|
||||
}
|
||||
@@ -39,9 +39,9 @@
|
||||
<CheckBox fx:id="expertSettingsCheckBox" text="%addvaultwizard.new.expertSettings.enableExpertSettingsCheckbox" onAction="#toggleUseExpertSettings"/>
|
||||
<VBox spacing="6" visible="${expertSettingsCheckBox.selected}">
|
||||
<HBox spacing="2" HBox.hgrow="NEVER">
|
||||
<Label text="%addvaultwizard.new.expertSettings.shorteningThreshold.title"/>
|
||||
<Label text="%addvaultwizard.new.expertSettings.shorteningThreshold.title" labelFor="$shorteningThresholdTextField"/>
|
||||
<Region prefWidth="2"/>
|
||||
<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#openDocs">
|
||||
<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#openDocs" accessibleText="%addvaultwizard.new.expertSettings.shorteningThreshold.tooltip">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="QUESTION_CIRCLE" styleClass="glyph-icon-muted"/>
|
||||
</graphic>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</padding>
|
||||
<children>
|
||||
<VBox spacing="6">
|
||||
<FormattedLabel format="%changepassword.enterOldPassword" arg1="${controller.vault.displayName}" wrapText="true"/>
|
||||
<FormattedLabel format="%changepassword.enterOldPassword" arg1="${controller.vault.displayName}" wrapText="true" labelFor="$oldPasswordField"/>
|
||||
<NiceSecurePasswordField fx:id="oldPasswordField"/>
|
||||
</VBox>
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<Insets left="6"/>
|
||||
</padding>
|
||||
<Region HBox.hgrow="ALWAYS"/>
|
||||
<Button styleClass="button-right" contentDisplay="GRAPHIC_ONLY" onAction="#copyTableToClipboard">
|
||||
<Button styleClass="button-right" contentDisplay="GRAPHIC_ONLY" onAction="#copyTableToClipboard" accessibleText="%decryptNames.copyTable.tooltip">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="CLIPBOARD" glyphSize="16"/>
|
||||
</graphic>
|
||||
@@ -30,7 +30,7 @@
|
||||
<Tooltip text="%decryptNames.copyTable.tooltip"/>
|
||||
</tooltip>
|
||||
</Button>
|
||||
<Button styleClass="button-right" contentDisplay="GRAPHIC_ONLY" onAction="#clearTable">
|
||||
<Button styleClass="button-right" contentDisplay="GRAPHIC_ONLY" onAction="#clearTable" accessibleText="%decryptNames.clearTable.tooltip">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="TRASH" glyphSize="16"/>
|
||||
</graphic>
|
||||
@@ -48,12 +48,12 @@
|
||||
</Button>
|
||||
</placeholder>
|
||||
<columns>
|
||||
<TableColumn fx:id="ciphertextColumn" prefWidth="${cipherToCleartextTable.width * 0.5}">
|
||||
<TableColumn fx:id="ciphertextColumn" prefWidth="${cipherToCleartextTable.width * 0.5}" text="%decryptNames.column.encrypted">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="LOCK"/>
|
||||
</graphic>
|
||||
</TableColumn>
|
||||
<TableColumn fx:id="cleartextColumn" prefWidth="${cipherToCleartextTable.width * 0.5}">
|
||||
<TableColumn fx:id="cleartextColumn" prefWidth="${cipherToCleartextTable.width * 0.5}" text="%decryptNames.column.decrypted">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="LOCK_OPEN"/>
|
||||
</graphic>
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<padding>
|
||||
<Insets top="6" bottom="6"/>
|
||||
</padding>
|
||||
<Label text="%error.technicalDetails"/>
|
||||
<Label text="%error.technicalDetails" labelFor="$detailsTextArea"/>
|
||||
<Region HBox.hgrow="ALWAYS"/>
|
||||
<Hyperlink styleClass="hyperlink-underline" text="%generic.button.copy" onAction="#copyDetails" contentDisplay="LEFT" visible="${!controller.copiedDetails}" managed="${!controller.copiedDetails}">
|
||||
<graphic>
|
||||
@@ -92,7 +92,7 @@
|
||||
</graphic>
|
||||
</Hyperlink>
|
||||
</HBox>
|
||||
<TextArea VBox.vgrow="ALWAYS" text="${controller.detailText}" prefRowCount="5" editable="false"/>
|
||||
<TextArea fx:id="detailsTextArea" VBox.vgrow="ALWAYS" text="${controller.detailText}" prefRowCount="5" editable="false"/>
|
||||
|
||||
<Region minHeight="18"/>
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="B+C">
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
<padding>
|
||||
<Insets left="6" />
|
||||
</padding>
|
||||
<ChoiceBox fx:id="vaultFilterChoiceBox" minWidth="42"/>
|
||||
<ChoiceBox fx:id="vaultFilterChoiceBox" minWidth="42" accessibleText="%eventView.filterVaults"/>
|
||||
<Region HBox.hgrow="ALWAYS"/>
|
||||
<Button styleClass="button-right" onAction="#clearEvents" contentDisplay="GRAPHIC_ONLY">
|
||||
<Button styleClass="button-right" onAction="#clearEvents" contentDisplay="GRAPHIC_ONLY" accessibleText="%eventView.clearListButton.tooltip">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="TRASH" glyphSize="16"/>
|
||||
</graphic>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ContextMenu?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.Tooltip?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<HBox xmlns:fx="http://javafx.com/fxml"
|
||||
@@ -29,10 +30,13 @@
|
||||
</HBox>
|
||||
<Label text="${controller.description}"/>
|
||||
</VBox>
|
||||
<Button fx:id="eventActionsButton" contentDisplay="GRAPHIC_ONLY" onAction="#toggleEventActionsMenu" managed="${controller.actionsButtonVisible}" visible="${controller.actionsButtonVisible}">
|
||||
<Button fx:id="eventActionsButton" contentDisplay="GRAPHIC_ONLY" onAction="#toggleEventActionsMenu" managed="${controller.actionsButtonVisible}" visible="${controller.actionsButtonVisible}" accessibleText="%eventView.cell.actionsButton.tooltip">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="ELLIPSIS_V" glyphSize="16"/>
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="%eventView.cell.actionsButton.tooltip"/>
|
||||
</tooltip>
|
||||
</Button>
|
||||
<VBox alignment="CENTER" maxWidth="64" minWidth="64" visible="${!controller.actionsButtonVisible}" managed="${!controller.actionsButtonVisible}">
|
||||
<Label text="${controller.eventLocalTime}" />
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
<FontAwesome5IconView glyph="FUNNEL" glyphSize="${filterLbl.height}" styleClass="glyph-icon-muted"/>
|
||||
</graphic>
|
||||
</Label>
|
||||
<ChoiceBox fx:id="severityChoiceBox" />
|
||||
<ChoiceBox fx:id="fixStateChoiceBox" />
|
||||
<ChoiceBox fx:id="severityChoiceBox" accessibleText="%health.check.detail.filterSeverity"/>
|
||||
<ChoiceBox fx:id="fixStateChoiceBox" accessibleText="%health.check.detail.filterFixState"/>
|
||||
</HBox>
|
||||
<ListView fx:id="resultsListView" VBox.vgrow="ALWAYS" visible="${!controller.checkSkipped}" fixedCellSize="25">
|
||||
<contextMenu>
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
<CheckBox fx:id="checkbox" visible="${controller.checkRunnable}"/>
|
||||
<CheckStateIconView check="${controller.check}" glyphSize="20" visible="${!controller.checkRunnable}"/>
|
||||
</StackPane>
|
||||
<Label text="${controller.checkName}"/>
|
||||
<Label text="${controller.checkName}" labelFor="$checkbox"/>
|
||||
|
||||
</HBox>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</padding>
|
||||
<children>
|
||||
<VBox spacing="6" visible="${!controller.vault.processing}" managed="${!controller.vault.processing}">
|
||||
<FormattedLabel format="%migration.run.enterPassword" arg1="${controller.vault.displayName}" wrapText="true"/>
|
||||
<FormattedLabel format="%migration.run.enterPassword" arg1="${controller.vault.displayName}" wrapText="true" labelFor="$passwordField"/>
|
||||
<NiceSecurePasswordField fx:id="passwordField"/>
|
||||
</VBox>
|
||||
|
||||
|
||||
@@ -26,6 +26,6 @@
|
||||
</VBox>
|
||||
</HBox>
|
||||
|
||||
<TextArea text="${controller.thirdPartyLicenseText}" editable="false"/>
|
||||
<TextArea text="${controller.thirdPartyLicenseText}" editable="false" accessibleText="%preferences.about.thirdPartyLicenses"/>
|
||||
</children>
|
||||
</VBox>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<?import javafx.scene.control.Hyperlink?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.TextArea?>
|
||||
<?import javafx.scene.control.Tooltip?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
@@ -32,10 +33,13 @@
|
||||
<FormattedLabel format="%preferences.contribute.registeredFor" arg1="${controller.licenseHolder.licenseSubject}" wrapText="true"/>
|
||||
<Region minHeight="12"/>
|
||||
<HBox alignment="BOTTOM_CENTER" spacing="6">
|
||||
<Button onAction="#didClickRemoveCert">
|
||||
<Button onAction="#didClickRemoveCert" contentDisplay="GRAPHIC_ONLY" accessibleText="%preferences.contribute.removeCert.tooltip">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="TRASH"/>
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="%preferences.contribute.removeCert.tooltip"/>
|
||||
</tooltip>
|
||||
</Button>
|
||||
<Button text="%preferences.contribute.donate" minWidth="100" onAction="#showDonate">
|
||||
<graphic>
|
||||
@@ -56,7 +60,7 @@
|
||||
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="HAND_HOLDING_HEART" glyphSize="24"/>
|
||||
</StackPane>
|
||||
<VBox HBox.hgrow="ALWAYS" spacing="6">
|
||||
<Label text="%preferences.contribute.noCertificate" wrapText="true" VBox.vgrow="ALWAYS"/>
|
||||
<Label text="%preferences.contribute.noCertificate" wrapText="true" VBox.vgrow="ALWAYS" labelFor="$supporterCertificateField"/>
|
||||
<Hyperlink text="%preferences.contribute.getCertificate" onAction="#getSupporterCertificate">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="LINK"/>
|
||||
|
||||
@@ -27,12 +27,12 @@
|
||||
|
||||
<HBox spacing="12" alignment="CENTER_LEFT">
|
||||
<CheckBox fx:id="useKeychainCheckbox" text="%preferences.general.keychainBackend"/>
|
||||
<ChoiceBox fx:id="keychainBackendChoiceBox"/>
|
||||
<ChoiceBox fx:id="keychainBackendChoiceBox" accessibleText="%preferences.general.keychainBackend"/>
|
||||
</HBox>
|
||||
|
||||
<HBox spacing="12" alignment="CENTER_LEFT" visible="${controller.someQuickAccessServiceAvailable}" managed="${controller.someQuickAccessServiceAvailable}">
|
||||
<CheckBox fx:id="useQuickAccessCheckbox" text="%preferences.general.quickAccessService"/>
|
||||
<ChoiceBox fx:id="quickAccessServiceChoiceBox"/>
|
||||
<ChoiceBox fx:id="quickAccessServiceChoiceBox" accessibleText="%preferences.general.quickAccessService"/>
|
||||
</HBox>
|
||||
<Region VBox.vgrow="ALWAYS"/>
|
||||
|
||||
|
||||
@@ -21,13 +21,13 @@
|
||||
</padding>
|
||||
<children>
|
||||
<HBox spacing="12" alignment="CENTER_LEFT">
|
||||
<Label text="%preferences.interface.theme"/>
|
||||
<Label text="%preferences.interface.theme" labelFor="$themeChoiceBox"/>
|
||||
<ChoiceBox fx:id="themeChoiceBox" disable="${!controller.licenseHolder.validLicense}"/>
|
||||
<Hyperlink styleClass="hyperlink-underline,hyperlink-muted" text="%preferences.interface.unlockThemes" onAction="#showContributeTab" visible="${!controller.licenseHolder.validLicense}" managed="${!controller.licenseHolder.validLicense}"/>
|
||||
</HBox>
|
||||
|
||||
<HBox spacing="12" alignment="CENTER_LEFT">
|
||||
<Label text="%preferences.interface.language"/>
|
||||
<Label text="%preferences.interface.language" labelFor="$preferredLanguageChoiceBox"/>
|
||||
<ChoiceBox fx:id="preferredLanguageChoiceBox"/>
|
||||
</HBox>
|
||||
|
||||
|
||||
@@ -3,55 +3,73 @@
|
||||
<?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 javafx.scene.control.ProgressBar?>
|
||||
<?import javafx.scene.control.Tooltip?>
|
||||
<?import javafx.scene.text.TextFlow?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.text.Text?>
|
||||
<?import javafx.scene.text.TextFlow?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.preferences.UpdatesPreferencesController"
|
||||
spacing="12">
|
||||
<fx:define>
|
||||
<FormattedString fx:id="linkLabel" format="%preferences.updates.updateAvailable" arg1="${controller.latestVersion}"/>
|
||||
</fx:define>
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="24"/>
|
||||
</padding>
|
||||
<FormattedLabel format="%preferences.updates.currentVersion" arg1="${controller.currentVersion}" textAlignment="CENTER" wrapText="true"/>
|
||||
<FormattedLabel format="%preferences.updates.currentVersion" arg1="${controller.updateChecker.currentVersion}" textAlignment="CENTER" wrapText="true"/>
|
||||
|
||||
<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}">
|
||||
<FormattedLabel format="%preferences.updates.updateAvailable" arg1="${controller.updateChecker.latestVersion}" textAlignment="CENTER" wrapText="true" visible="${controller.updateChecker.updateAvailable}"/>
|
||||
|
||||
<Button text="${controller.updateButtonTitle}" defaultButton="true" onAction="#startWork" disable="${controller.updateButtonDisabled}" contentDisplay="${controller.updateButtonState}">
|
||||
<graphic>
|
||||
<FontAwesome5Spinner glyphSize="12"/>
|
||||
<VBox spacing="5" alignment="CENTER">
|
||||
<ProgressBar maxWidth="200"
|
||||
maxHeight="12"
|
||||
visible="${controller.running && controller.worker.progress != -1.0}"
|
||||
managed="${controller.running && controller.worker.progress != -1.0}"
|
||||
progress="${controller.worker.progress}"/>
|
||||
<FontAwesome5Spinner glyphSize="12"
|
||||
visible="${controller.running && controller.worker.progress == -1.0}"
|
||||
managed="${controller.running && controller.worker.progress == -1.0}"/>
|
||||
</VBox>
|
||||
</graphic>
|
||||
</Button>
|
||||
|
||||
<TextFlow styleClass="text-flow" textAlignment="CENTER" visible="${controller.checkFailed}" managed="${controller.checkFailed}">
|
||||
<TextFlow styleClass="text-flow" textAlignment="CENTER" visible="${controller.prohibitUpdateWhileUnlocked}" managed="${controller.prohibitUpdateWhileUnlocked}">
|
||||
<FontAwesome5IconView glyphSize="12" styleClass="glyph-icon-primary" glyph="LOCK_OPEN"/>
|
||||
<Text text=" "/>
|
||||
<Text text="%preferences.updates.prohibitedDueToUnlockedVaults.1"/>
|
||||
<Text text=" "/>
|
||||
<Hyperlink styleClass="hyperlink-underline" text="%preferences.updates.prohibitedDueToUnlockedVaults.2" onAction="#lockAllGracefully"/>
|
||||
<Text text=" "/>
|
||||
<Text text="%preferences.updates.prohibitedDueToUnlockedVaults.3"/>
|
||||
</TextFlow>
|
||||
|
||||
<TextFlow styleClass="text-flow" textAlignment="CENTER" visible="${!controller.errorMessage.empty}" managed="${!controller.errorMessage.empty}">
|
||||
<FontAwesome5IconView glyphSize="12" styleClass="glyph-icon-orange" glyph="EXCLAMATION_TRIANGLE"/>
|
||||
<Text text=" "/>
|
||||
<Text text="%preferences.updates.checkFailed"/>
|
||||
<Text text="${controller.errorMessage}"/>
|
||||
<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">
|
||||
|
||||
<FormattedLabel format="%preferences.updates.lastUpdateCheck" arg1="${controller.timeDifferenceMessage}" textAlignment="CENTER" wrapText="true" visible="${!controller.updateChecker.updateAvailable}" managed="${!controller.updateChecker.updateAvailable}">
|
||||
<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>
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
</padding>
|
||||
<children>
|
||||
<HBox spacing="12" alignment="CENTER_LEFT">
|
||||
<Label text="%preferences.volume.type"/>
|
||||
<Label text="%preferences.volume.type" labelFor="$volumeTypeChoiceBox"/>
|
||||
<ChoiceBox fx:id="volumeTypeChoiceBox"/>
|
||||
<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#openDocs">
|
||||
<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#openDocs" accessibleText="%preferences.volume.docsTooltip">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="QUESTION_CIRCLE" styleClass="glyph-icon-muted"/>
|
||||
</graphic>
|
||||
@@ -33,7 +33,7 @@
|
||||
</HBox>
|
||||
|
||||
<HBox spacing="12" alignment="CENTER_LEFT" visible="${controller.loopbackPortSupported}" managed="${controller.loopbackPortSupported}">
|
||||
<Label text="%preferences.volume.tcp.port"/>
|
||||
<Label text="%preferences.volume.tcp.port" labelFor="$loopbackPortField"/>
|
||||
<NumericTextField fx:id="loopbackPortField"/>
|
||||
<Button text="%generic.button.apply" fx:id="loopbackPortApplyButton" onAction="#doChangeLoopbackPort"/>
|
||||
</HBox>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<Insets bottom="6" top="6"/>
|
||||
</padding>
|
||||
</Label>
|
||||
<FormattedLabel fx:id="descriptionLabel" format="%recoveryKey.create.description" arg1="${controller.vault.displayName}" wrapText="true">
|
||||
<FormattedLabel fx:id="descriptionLabel" format="%recoveryKey.create.description" arg1="${controller.vault.displayName}" wrapText="true" labelFor="$passwordField">
|
||||
<padding>
|
||||
<Insets bottom="6"/>
|
||||
</padding>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
spacing="12"
|
||||
alignment="TOP_LEFT">
|
||||
<children>
|
||||
<FormattedLabel format="%recoveryKey.display.description" arg1="${controller.vaultName}" wrapText="true"/>
|
||||
<FormattedLabel format="%recoveryKey.display.description" arg1="${controller.vaultName}" wrapText="true" labelFor="$textarea"/>
|
||||
|
||||
<TextArea editable="false" text="${controller.recoveryKey}" wrapText="true" prefRowCount="4" fx:id="textarea"/>
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="+R">
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<HBox spacing="2" HBox.hgrow="NEVER">
|
||||
<Label text="%addvaultwizard.new.expertSettings.shorteningThreshold.title"/>
|
||||
<Region prefWidth="2"/>
|
||||
<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#openDocs">
|
||||
<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#openDocs" accessibleText="%addvaultwizard.new.expertSettings.shorteningThreshold.tooltip">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="QUESTION_CIRCLE" styleClass="glyph-icon-muted"/>
|
||||
</graphic>
|
||||
@@ -57,7 +57,7 @@
|
||||
</tooltip>
|
||||
</Hyperlink>
|
||||
</HBox>
|
||||
<Label text="%recover.expertSettings.shorteningThreshold.title" wrapText="true"/>
|
||||
<Label text="%recover.expertSettings.shorteningThreshold.title" wrapText="true" labelFor="$shorteningThresholdTextField"/>
|
||||
<NumericTextField fx:id="shorteningThresholdTextField"/>
|
||||
<HBox alignment="TOP_RIGHT">
|
||||
<Region minWidth="4" prefWidth="4" HBox.hgrow="NEVER"/>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
minHeight="145"
|
||||
spacing="12">
|
||||
<children>
|
||||
<FormattedLabel format="%recoveryKey.recover.prompt" arg1="${controller.vault.displayName}" wrapText="true"/>
|
||||
<FormattedLabel format="%recoveryKey.recover.prompt" arg1="${controller.vault.displayName}" wrapText="true" labelFor="$textarea"/>
|
||||
|
||||
<TextArea wrapText="true" prefRowCount="4" fx:id="textarea" textFormatter="${controller.recoveryKeyTextFormatter}" onKeyPressed="#onKeyPressed"/>
|
||||
<VBox>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<TextFlow styleClass="text-flow">
|
||||
<Text text="%shareVault.remarkBestPractices"/>
|
||||
<Text text=" "/>
|
||||
<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#visitBestPractices">
|
||||
<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#visitBestPractices" accessibleText="%shareVault.docsTooltip">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="QUESTION_CIRCLE" styleClass="glyph-icon-muted"/>
|
||||
</graphic>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</ImageView>
|
||||
</StackPane>
|
||||
<VBox spacing="6" HBox.hgrow="ALWAYS">
|
||||
<FormattedLabel format="%unlock.passwordPrompt" arg1="${controller.vaultName}" wrapText="true"/>
|
||||
<FormattedLabel format="%unlock.passwordPrompt" arg1="${controller.vaultName}" wrapText="true" labelFor="$passwordField"/>
|
||||
<NiceSecurePasswordField fx:id="passwordField" disable="${controller.userInteractionDisabled}"/>
|
||||
<CheckBox fx:id="savePasswordCheckbox" text="%unlock.savePassword" onAction="#didClickSavePasswordCheckbox" disable="${controller.userInteractionDisabled}" visible="${controller.keychainAccessAvailable}"/>
|
||||
</VBox>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
alignment="TOP_CENTER"
|
||||
spacing="9">
|
||||
<Label text="%main.vaultDetail.accessLocation"/>
|
||||
<Button styleClass="button-large" contentDisplay="GRAPHIC_ONLY" minWidth="120" onAction="#revealAccessLocation" defaultButton="${controller.accessibleViaPath}" visible="${controller.accessibleViaPath}" managed="${controller.accessibleViaPath}">
|
||||
<Button styleClass="button-large" contentDisplay="GRAPHIC_ONLY" minWidth="120" onAction="#revealAccessLocation" defaultButton="${controller.accessibleViaPath}" visible="${controller.accessibleViaPath}" managed="${controller.accessibleViaPath}" accessibleText="%main.vaultDetail.revealBtn">
|
||||
<graphic>
|
||||
<HBox spacing="12" alignment="CENTER">
|
||||
<FontAwesome5IconView glyph="HDD" glyphSize="24"/>
|
||||
@@ -25,8 +25,11 @@
|
||||
</VBox>
|
||||
</HBox>
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="%main.vaultDetail.revealBtn"/>
|
||||
</tooltip>
|
||||
</Button>
|
||||
<Button styleClass="button-large" contentDisplay="GRAPHIC_ONLY" minWidth="120" onAction="#copyMountUri" defaultButton="${controller.accessibleViaUri}" visible="${controller.accessibleViaUri}" managed="${controller.accessibleViaUri}">
|
||||
<Button styleClass="button-large" contentDisplay="GRAPHIC_ONLY" minWidth="120" onAction="#copyMountUri" defaultButton="${controller.accessibleViaUri}" visible="${controller.accessibleViaUri}" managed="${controller.accessibleViaUri}" accessibleText="%main.vaultDetail.copyUri">
|
||||
<graphic>
|
||||
<HBox spacing="12" alignment="CENTER">
|
||||
<FontAwesome5IconView glyph="LINK" glyphSize="24"/>
|
||||
@@ -36,6 +39,9 @@
|
||||
</VBox>
|
||||
</HBox>
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="%main.vaultDetail.copyUri"/>
|
||||
</tooltip>
|
||||
</Button>
|
||||
<Button text="%main.vaultDetail.lockBtn" minWidth="120" onAction="#lock">
|
||||
<graphic>
|
||||
|
||||
@@ -38,14 +38,17 @@
|
||||
</VBox>
|
||||
</StackPane>
|
||||
<HBox styleClass="button-bar">
|
||||
<Button fx:id="addVaultButton" onMouseClicked="#toggleMenu" styleClass="button-left" alignment="CENTER" minWidth="20" contentDisplay="GRAPHIC_ONLY">
|
||||
<Button fx:id="addVaultButton" onAction="#toggleMenu" styleClass="button-left" alignment="CENTER" minWidth="20" contentDisplay="GRAPHIC_ONLY" accessibleText="%main.vaultlist.addVaultButton.tooltip">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="PLUS" glyphSize="16"/>
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="%main.vaultlist.addVaultButton.tooltip"/>
|
||||
</tooltip>
|
||||
</Button>
|
||||
<Region HBox.hgrow="ALWAYS"/>
|
||||
<StackPane>
|
||||
<Button onMouseClicked="#showEventViewer" styleClass="button-right" minWidth="20" contentDisplay="GRAPHIC_ONLY" mnemonicParsing="false">
|
||||
<Button onAction="#showEventViewer" styleClass="button-right" minWidth="20" contentDisplay="GRAPHIC_ONLY" mnemonicParsing="false" accessibleText="%main.vaultlist.showEventsButton.tooltip">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="BELL" glyphSize="16"/>
|
||||
</graphic>
|
||||
@@ -57,10 +60,13 @@
|
||||
<Circle radius="4" styleClass="icon-update-indicator" AnchorPane.topAnchor="-8" AnchorPane.rightAnchor="-6" visible="${controller.unreadEventsPresent}" />
|
||||
</AnchorPane>
|
||||
</StackPane>
|
||||
<Button onMouseClicked="#showPreferences" styleClass="button-right" alignment="CENTER" minWidth="20" contentDisplay="GRAPHIC_ONLY">
|
||||
<Button onAction="#showPreferences" styleClass="button-right" alignment="CENTER" minWidth="20" contentDisplay="GRAPHIC_ONLY" accessibleText="%main.vaultlist.showPreferencesButton.tooltip">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="COG" glyphSize="16"/>
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="%main.vaultlist.showPreferencesButton.tooltip"/>
|
||||
</tooltip>
|
||||
</Button>
|
||||
</HBox>
|
||||
</VBox>
|
||||
|
||||
@@ -22,14 +22,14 @@
|
||||
</padding>
|
||||
<children>
|
||||
<HBox spacing="6" alignment="CENTER_LEFT">
|
||||
<Label text="%vaultOptions.general.vaultName"/>
|
||||
<Label text="%vaultOptions.general.vaultName" labelFor="$vaultName"/>
|
||||
<TextField fx:id="vaultName"/>
|
||||
</HBox>
|
||||
|
||||
<TextFlow styleClass="text-flow" prefWidth="-Infinity">
|
||||
<CheckBox text="%vaultOptions.general.autoLock.lockAfterTimePart1" fx:id="lockAfterTimeCheckbox"/>
|
||||
<Text text=" "/>
|
||||
<NumericTextField fx:id="lockTimeInMinutesTextField" prefWidth="50"/>
|
||||
<NumericTextField fx:id="lockTimeInMinutesTextField" prefWidth="50" accessibleText="%vaultOptions.general.autoLock.accessibleText"/>
|
||||
<Text text=" "/>
|
||||
<FormattedLabel format="%vaultOptions.general.autoLock.lockAfterTimePart2"/>
|
||||
</TextFlow>
|
||||
@@ -37,7 +37,7 @@
|
||||
<CheckBox text="%vaultOptions.general.unlockAfterStartup" fx:id="unlockOnStartupCheckbox"/>
|
||||
|
||||
<HBox spacing="6" alignment="CENTER_LEFT">
|
||||
<Label text="%vaultOptions.general.actionAfterUnlock"/>
|
||||
<Label text="%vaultOptions.general.actionAfterUnlock" labelFor="$actionAfterUnlockChoiceBox"/>
|
||||
<ChoiceBox fx:id="actionAfterUnlockChoiceBox"/>
|
||||
</HBox>
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
</padding>
|
||||
<children>
|
||||
<HBox spacing="12" alignment="CENTER_LEFT">
|
||||
<Label text="%vaultOptions.mount.volume.type"/>
|
||||
<Label text="%vaultOptions.mount.volume.type" labelFor="$vaultVolumeTypeChoiceBox"/>
|
||||
<ChoiceBox fx:id="vaultVolumeTypeChoiceBox"/>
|
||||
<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#openVolumePreferences" visible="${controller.defaultMountServiceSelected}" managed="${controller.defaultMountServiceSelected}">
|
||||
<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#openVolumePreferences" visible="${controller.defaultMountServiceSelected}" managed="${controller.defaultMountServiceSelected}" accessibleText="%vaultOptions.mount.info">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="COGS" styleClass="glyph-icon-muted"/>
|
||||
</graphic>
|
||||
@@ -36,7 +36,7 @@
|
||||
<Tooltip text="%vaultOptions.mount.info" showDelay="100ms"/>
|
||||
</tooltip>
|
||||
</Hyperlink>
|
||||
<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#openDocs" visible="${!controller.defaultMountServiceSelected}" managed="${!controller.defaultMountServiceSelected}">
|
||||
<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#openDocs" visible="${!controller.defaultMountServiceSelected}" managed="${!controller.defaultMountServiceSelected}" accessibleText="%preferences.volume.docsTooltip">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="QUESTION_CIRCLE" styleClass="glyph-icon-muted"/>
|
||||
</graphic>
|
||||
@@ -49,7 +49,7 @@
|
||||
<Label styleClass="label-red" text="%vaultOptions.mount.volumeType.restartRequired" visible="${controller.selectedMountServiceRequiresRestart}" managed="${controller.selectedMountServiceRequiresRestart}"/>
|
||||
|
||||
<HBox spacing="12" alignment="CENTER_LEFT" visible="${controller.loopbackPortChangeable}" managed="${controller.loopbackPortChangeable}">
|
||||
<Label text="%vaultOptions.mount.volume.tcp.port"/>
|
||||
<Label text="%vaultOptions.mount.volume.tcp.port" labelFor="$vaultLoopbackPortField"/>
|
||||
<NumericTextField fx:id="vaultLoopbackPortField"/>
|
||||
<Button text="%generic.button.apply" fx:id="vaultLoopbackPortApplyButton" onAction="#doChangeLoopbackPort"/>
|
||||
</HBox>
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
<VBox visible="${controller.mountFlagsSupported}" managed="${controller.mountFlagsSupported}">
|
||||
<CheckBox fx:id="customMountFlagsCheckbox" text="%vaultOptions.mount.customMountFlags" onAction="#toggleUseCustomMountFlags"/>
|
||||
<TextField fx:id="mountFlagsField" HBox.hgrow="ALWAYS" maxWidth="Infinity">
|
||||
<TextField fx:id="mountFlagsField" HBox.hgrow="ALWAYS" maxWidth="Infinity" accessibleText="%vaultOptions.mount.customMountFlags">
|
||||
<VBox.margin>
|
||||
<Insets left="24"/>
|
||||
</VBox.margin>
|
||||
@@ -75,7 +75,7 @@
|
||||
|
||||
<HBox spacing="6" visible="${controller.mountpointDriveLetterSupported}" managed="${controller.mountpointDriveLetterSupported}">
|
||||
<RadioButton toggleGroup="${mountPointToggleGroup}" fx:id="mountPointDriveLetterBtn" text="%vaultOptions.mount.mountPoint.driveLetter"/>
|
||||
<ChoiceBox fx:id="driveLetterSelection" disable="${!mountPointDriveLetterBtn.selected}"/>
|
||||
<ChoiceBox fx:id="driveLetterSelection" disable="${!mountPointDriveLetterBtn.selected}" accessibleText="%vaultOptions.mount.mountPoint.driveLetter"/>
|
||||
</HBox>
|
||||
|
||||
<VBox spacing="6" visible="${controller.mountpointDirSupported}" managed="${controller.mountpointDirSupported}">
|
||||
@@ -87,7 +87,7 @@
|
||||
</graphic>
|
||||
</Button>
|
||||
</HBox>
|
||||
<TextField fx:id="directoryPathField" text="${controller.directoryPath}" visible="${mountPointDirBtn.selected}" managed="${mountPointDirBtn.managed}" maxWidth="Infinity" editable="false">
|
||||
<TextField fx:id="directoryPathField" text="${controller.directoryPath}" visible="${mountPointDirBtn.selected}" managed="${mountPointDirBtn.managed}" maxWidth="Infinity" editable="false" accessibleText="%vaultOptions.mount.mountPoint.custom">
|
||||
<VBox.margin>
|
||||
<Insets left="24"/>
|
||||
</VBox.margin>
|
||||
|
||||
@@ -258,6 +258,8 @@ health.check.detail.checkFinishedAndFound=The check finished running. Please rev
|
||||
health.check.detail.checkFailed=The check exited due to an error.
|
||||
health.check.detail.checkCancelled=The check was cancelled.
|
||||
health.check.detail.listFilters.label=Filter
|
||||
health.check.detail.filterSeverity=Filter by severity
|
||||
health.check.detail.filterFixState=Filter by fix state
|
||||
health.check.detail.fixAllSpecificBtn=Fix all of type
|
||||
health.check.exportBtn=Export Report
|
||||
## Result view
|
||||
@@ -330,8 +332,13 @@ 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.prohibitedDueToUnlockedVaults.1=Please
|
||||
preferences.updates.prohibitedDueToUnlockedVaults.2=lock your vaults
|
||||
preferences.updates.prohibitedDueToUnlockedVaults.3=to install the update.
|
||||
preferences.updates.checkFailed=Looking for updates failed. Please check your internet connection or try again later.
|
||||
preferences.updates.updateFailed=Update failed. Please install the update manually.
|
||||
preferences.updates.upToDate=Cryptomator is up-to-date.
|
||||
preferences.updates.visitDownloadPage=Visit Download Page
|
||||
|
||||
## Contribution
|
||||
preferences.contribute=Support Us
|
||||
@@ -342,6 +349,7 @@ preferences.contribute.promptText=Paste supporter certificate code here
|
||||
preferences.contribute.thankYou=Thank you for supporting Cryptomator's open-source development!
|
||||
preferences.contribute.donate=Donate
|
||||
preferences.contribute.sponsor=Sponsor
|
||||
preferences.contribute.removeCert.tooltip=Remove certificate
|
||||
|
||||
### Remove License Key Dialog
|
||||
removeCert.title=Remove Certificate
|
||||
@@ -351,6 +359,7 @@ removeCert.description=Cryptomator's core features are not affected by this. Nei
|
||||
|
||||
## About
|
||||
preferences.about=About
|
||||
preferences.about.thirdPartyLicenses=Third-party licenses
|
||||
|
||||
# Vault Statistics
|
||||
stats.title=Statistics for %s
|
||||
@@ -400,7 +409,9 @@ main.vaultlist.contextMenu.share=Share…
|
||||
main.vaultlist.addVaultBtn.menuItemNew=Create New Vault…
|
||||
main.vaultlist.addVaultBtn.menuItemExisting=Open Existing Vault…
|
||||
main.vaultlist.addVaultBtn.menuItemRecover=Recover Existing Vault…
|
||||
main.vaultlist.showEventsButton.tooltip=Open event view
|
||||
main.vaultlist.addVaultButton.tooltip=Add Vault
|
||||
main.vaultlist.showEventsButton.tooltip=Open Event View
|
||||
main.vaultlist.showPreferencesButton.tooltip=Show Preferences
|
||||
##Notificaition
|
||||
main.notification.updateAvailable=Update is available.
|
||||
main.notification.support=Support Cryptomator.
|
||||
@@ -464,6 +475,7 @@ vaultOptions.general=General
|
||||
vaultOptions.general.vaultName=Vault Name
|
||||
vaultOptions.general.autoLock.lockAfterTimePart1=Lock when idle for
|
||||
vaultOptions.general.autoLock.lockAfterTimePart2=minutes
|
||||
vaultOptions.general.autoLock.accessibleText=Lock timeout in minutes
|
||||
vaultOptions.general.unlockAfterStartup=Unlock vault when starting Cryptomator
|
||||
vaultOptions.general.actionAfterUnlock=After successful unlock
|
||||
vaultOptions.general.actionAfterUnlock.ignore=Do nothing
|
||||
@@ -647,6 +659,8 @@ decryptNames.filePicker.title=Select encrypted file
|
||||
decryptNames.filePicker.extensionDescription=Cryptomator encrypted file
|
||||
decryptNames.copyTable.tooltip=Copy table
|
||||
decryptNames.clearTable.tooltip=Clear table
|
||||
decryptNames.column.encrypted=Encrypted
|
||||
decryptNames.column.decrypted=Decrypted
|
||||
decryptNames.copyHint=Copy cell content with %s
|
||||
decryptNames.dropZone.message=Drop files or click to select
|
||||
decryptNames.dropZone.error.vaultInternalFiles=Vault internal files with no decrypt-able name selected
|
||||
@@ -659,6 +673,8 @@ decryptNames.dropZone.error.generic=Failed to decrypt file names
|
||||
eventView.title=Events
|
||||
eventView.filter.allVaults=All
|
||||
eventView.clearListButton.tooltip=Clear list
|
||||
eventView.filterVaults=Filter by vault
|
||||
eventView.cell.actionsButton.tooltip=Event actions
|
||||
## event list entries
|
||||
eventView.entry.vaultLocked.description=Unlock "%s" for details
|
||||
eventView.entry.conflictResolved.message=Resolved conflict
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
|
||||
* All rights reserved.
|
||||
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class SemVerComparatorTest {
|
||||
|
||||
private final Comparator<String> semVerComparator = new SemVerComparator();
|
||||
|
||||
// equal versions
|
||||
|
||||
@Test
|
||||
public void compareEqualVersions() {
|
||||
Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4")));
|
||||
Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-alpha")));
|
||||
Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4+20170101", "1.23.4+20171231")));
|
||||
Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4-alpha+20170101", "1.23.4-alpha+20171231")));
|
||||
}
|
||||
|
||||
// newer versions in first argument
|
||||
|
||||
@Test
|
||||
public void compareHigherToLowerVersions() {
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.5", "1.23.4")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.24.4", "1.23.4")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4-SNAPSHOT")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4-56.78")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-beta", "1.23.4-alpha")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-alpha.1", "1.23.4-alpha")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-56.79", "1.23.4-56.78")));
|
||||
}
|
||||
|
||||
// newer versions in second argument
|
||||
|
||||
@Test
|
||||
public void compareLowerToHigherVersions() {
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.5")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.24.4")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23", "1.23.4")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-SNAPSHOT", "1.23.4")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-56.78", "1.23.4")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-beta")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-alpha.1")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-56.78", "1.23.4-56.79")));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,31 +9,28 @@ import org.cryptomator.common.Environment;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class SettingsTest {
|
||||
|
||||
@Test
|
||||
public void testAutoSave() {
|
||||
Environment env = Mockito.mock(Environment.class);
|
||||
@SuppressWarnings("unchecked") Consumer<Settings> changeListener = Mockito.mock(Consumer.class);
|
||||
SettingsProvider provider = Mockito.mock(SettingsProvider.class);
|
||||
|
||||
Settings settings = Settings.create(env);
|
||||
settings.setSaveCmd(changeListener);
|
||||
Settings settings = Settings.create(provider, env);
|
||||
VaultSettings vaultSettings = VaultSettings.withRandomId();
|
||||
Mockito.verify(changeListener, Mockito.times(0)).accept(settings);
|
||||
Mockito.verify(provider, Mockito.times(0)).scheduleSave(settings);
|
||||
|
||||
// first change (to property):
|
||||
settings.port.set(42428);
|
||||
Mockito.verify(changeListener, Mockito.times(1)).accept(settings);
|
||||
Mockito.verify(provider, Mockito.times(1)).scheduleSave(settings);
|
||||
|
||||
// second change (to list):
|
||||
settings.directories.add(vaultSettings);
|
||||
Mockito.verify(changeListener, Mockito.times(2)).accept(settings);
|
||||
Mockito.verify(provider, Mockito.times(2)).scheduleSave(settings);
|
||||
|
||||
// third change (to property of list item):
|
||||
vaultSettings.displayName.set("asd");
|
||||
Mockito.verify(changeListener, Mockito.times(3)).accept(settings);
|
||||
Mockito.verify(provider, Mockito.times(3)).scheduleSave(settings);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user