mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-09-22 07:54:19 +00:00
Compare commits
276
Commits
1.4.17
...
1.5.0-alpha1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cd3a6dbaf | ||
|
|
9934a4ea3a | ||
|
|
3468a88268 | ||
|
|
4d916cd5cc | ||
|
|
24f13e2afe | ||
|
|
ad29c009b2 | ||
|
|
d87955adb6 | ||
|
|
07aa71bb9d | ||
|
|
ea3ef8b60f | ||
|
|
a33df54ed3 | ||
|
|
3107f16768 | ||
|
|
230436be8a | ||
|
|
7fefeee145 | ||
|
|
db836a9dc3 | ||
|
|
ba22f0ed3c | ||
|
|
a731b1b569 | ||
|
|
8bb3530928 | ||
|
|
2567ca50e7 | ||
|
|
8d00b926c4 | ||
|
|
200fc1a563 | ||
|
|
c961eb0ca0 | ||
|
|
d2086d100e | ||
|
|
2a33705cc6 | ||
|
|
4b48f75aed | ||
|
|
2bbc3e5834 | ||
|
|
9e14b5e70f | ||
|
|
161a4cd511 | ||
|
|
2cf97b5f77 | ||
|
|
0d6df6e6c1 | ||
|
|
6a4edbf73d | ||
|
|
d6ffb890e3 | ||
|
|
5b83456e6d | ||
|
|
3ad6784961 | ||
|
|
3fa1ed1928 | ||
|
|
d27466d49e | ||
|
|
d9677fe7c4 | ||
|
|
fa7421b1b0 | ||
|
|
602bccae2a | ||
|
|
0acd3b427f | ||
|
|
f35f04851e | ||
|
|
cb4717b770 | ||
|
|
68e69790cf | ||
|
|
2bd93469bb | ||
|
|
6fc37d48ff | ||
|
|
df8fbe6440 | ||
|
|
9eee7883c5 | ||
|
|
7de08f52df | ||
|
|
b4bf5415bc | ||
|
|
c8e22fe2e3 | ||
|
|
af89cee619 | ||
|
|
216f3620e1 | ||
|
|
ae97e42d11 | ||
|
|
c6ad677e2b | ||
|
|
e1a1f0bbdb | ||
|
|
efaf5a1553 | ||
|
|
9c104beeba | ||
|
|
31bad3191d | ||
|
|
1089497c08 | ||
|
|
73bc2d06a7 | ||
|
|
040d913693 | ||
|
|
52a488f967 | ||
|
|
7511a676d4 | ||
|
|
6b7964f5d9 | ||
|
|
98e843aa0d | ||
|
|
c364a743ae | ||
|
|
b36c5fa60f | ||
|
|
bd0ece8020 | ||
|
|
9b15254880 | ||
|
|
4f039b9708 | ||
|
|
2ab823239c | ||
|
|
78d4f2e479 | ||
|
|
6513fc6ed4 | ||
|
|
e41ddedce7 | ||
|
|
88220cabee | ||
|
|
33252c1775 | ||
|
|
57553bbda1 | ||
|
|
4a02bf529d | ||
|
|
e55b1f8ff9 | ||
|
|
73314ee985 | ||
|
|
5196dbe9af | ||
|
|
68454e914b | ||
|
|
d0924e246f | ||
|
|
59a277f0c7 | ||
|
|
5ef0dc5505 | ||
|
|
08434c36ee | ||
|
|
2f288dc92b | ||
|
|
421a21aced | ||
|
|
98a3a1a372 | ||
|
|
da65e98030 | ||
|
|
7bbbd3c849 | ||
|
|
0481bb02af | ||
|
|
c47d9c95c7 | ||
|
|
a122159268 | ||
|
|
474064df3a | ||
|
|
1f65a626e4 | ||
|
|
04800407d7 | ||
|
|
f306184b53 | ||
|
|
c1a8844f27 | ||
|
|
d4e118f331 | ||
|
|
2900a9672a | ||
|
|
e8ba94942d | ||
|
|
b3a96aed74 | ||
|
|
0db2068344 | ||
|
|
6da0d023ff | ||
|
|
88d522501f | ||
|
|
6b31e6cdc2 | ||
|
|
1ef8668c4d | ||
|
|
6721baf293 | ||
|
|
f4ec9c277f | ||
|
|
c9d897edd1 | ||
|
|
ef07edf8c3 | ||
|
|
36d49a6e1e | ||
|
|
f4ee8d0a15 | ||
|
|
71e414ae5c | ||
|
|
acda11f110 | ||
|
|
a96935bfb5 | ||
|
|
b74b4ce244 | ||
|
|
638dac9cb6 | ||
|
|
249af4c397 | ||
|
|
4130dbea91 | ||
|
|
8939b196c0 | ||
|
|
f1b5ec6481 | ||
|
|
50ace8c6a4 | ||
|
|
264e81b4a0 | ||
|
|
282b80fe24 | ||
|
|
547e5dae52 | ||
|
|
5dcdaf459f | ||
|
|
ab89f59dd1 | ||
|
|
98451759f0 | ||
|
|
b22ddaf13e | ||
|
|
7eaf598089 | ||
|
|
3d7b504504 | ||
|
|
4dc8693216 | ||
|
|
ac472393aa | ||
|
|
477fd7d2e9 | ||
|
|
468723ad92 | ||
|
|
d58ad1e0a2 | ||
|
|
8c94245102 | ||
|
|
b4f697e052 | ||
|
|
428ffbf705 | ||
|
|
d25a903556 | ||
|
|
3db906de10 | ||
|
|
486d3170e2 | ||
|
|
15bdba85bb | ||
|
|
db1a2538cb | ||
|
|
3d61fab8b6 | ||
|
|
644f2d9a89 | ||
|
|
46a4060814 | ||
|
|
5c26c23619 | ||
|
|
60bdafca94 | ||
|
|
fc51e5caff | ||
|
|
e32ddd07ea | ||
|
|
6b47cf54e2 | ||
|
|
40c3e7a417 | ||
|
|
4bfb543419 | ||
|
|
8c9d78337e | ||
|
|
0a132351a8 | ||
|
|
ded43f7e7b | ||
|
|
cf91a8b62c | ||
|
|
948659f3cd | ||
|
|
c7e0015ffd | ||
|
|
2fd671f513 | ||
|
|
5fa86fad5e | ||
|
|
c6d90cdb17 | ||
|
|
dcf44aa134 | ||
|
|
50b2b0dc57 | ||
|
|
111d20a1cf | ||
|
|
19840663a8 | ||
|
|
4b7527bf0c | ||
|
|
0a5bfa1ccb | ||
|
|
21e1d303aa | ||
|
|
e0d5641d26 | ||
|
|
529780f2c9 | ||
|
|
19f1093d2c | ||
|
|
5807e10286 | ||
|
|
0a1ad9f4d6 | ||
|
|
2a3e2e315e | ||
|
|
23c868b646 | ||
|
|
ed8af54324 | ||
|
|
d8c3ba1e36 | ||
|
|
17f45c6dab | ||
|
|
86745c5962 | ||
|
|
ca72c7a51f | ||
|
|
012c7cede9 | ||
|
|
15a55e996b | ||
|
|
cb50b2011f | ||
|
|
04c45756b9 | ||
|
|
8d08d81f45 | ||
|
|
d77403c403 | ||
|
|
1d94069144 | ||
|
|
6e10d6bcd7 | ||
|
|
7453431bcd | ||
|
|
2f953061b3 | ||
|
|
67610b935e | ||
|
|
09e5ee9e26 | ||
|
|
e23dc72856 | ||
|
|
b86d4b5a37 | ||
|
|
2024c95be5 | ||
|
|
d51debf736 | ||
|
|
70b4b5fb2d | ||
|
|
e32fbc1d79 | ||
|
|
be43279574 | ||
|
|
900e556369 | ||
|
|
09aca188fe | ||
|
|
fc9565e13c | ||
|
|
e7f7321393 | ||
|
|
ddbe1c4546 | ||
|
|
e3fa31b701 | ||
|
|
bfe8a22b48 | ||
|
|
dab779cbef | ||
|
|
66104bb753 | ||
|
|
37c69f991f | ||
|
|
610460b3e4 | ||
|
|
43750e2bad | ||
|
|
a7df100e56 | ||
|
|
4afb9d86c7 | ||
|
|
1b7a7060a8 | ||
|
|
1f6b94003e | ||
|
|
2f1a7821b0 | ||
|
|
1ec887092f | ||
|
|
71afb088b5 | ||
|
|
01ca501607 | ||
|
|
ba34b37f8a | ||
|
|
62ff954558 | ||
|
|
fead9a48eb | ||
|
|
10e842e457 | ||
|
|
4931e6707a | ||
|
|
036dca33be | ||
|
|
74565d7995 | ||
|
|
65ab09e63c | ||
|
|
8e305b0e21 | ||
|
|
ff66e07c65 | ||
|
|
4138747fc3 | ||
|
|
87c654b43d | ||
|
|
d0062b7e22 | ||
|
|
1d3900b86c | ||
|
|
b799ce34f6 | ||
|
|
8d6cf9581b | ||
|
|
1a5853766a | ||
|
|
cd284304cd | ||
|
|
363e4152a5 | ||
|
|
0ce4ddb874 | ||
|
|
3c574b97cb | ||
|
|
b7b8f26992 | ||
|
|
4eca5069b3 | ||
|
|
03618f547d | ||
|
|
12477c07d6 | ||
|
|
0fb2d445b0 | ||
|
|
43c5482542 | ||
|
|
dc8e7a86b0 | ||
|
|
2a69740559 | ||
|
|
dd0e548353 | ||
|
|
0eb19a287f | ||
|
|
14de8ffd59 | ||
|
|
ac6249f541 | ||
|
|
5af2a392ea | ||
|
|
edfdca6e5f | ||
|
|
c2ed9958c7 | ||
|
|
fddae7d0e1 | ||
|
|
fa729e220c | ||
|
|
c917fb6a57 | ||
|
|
cf2e026f75 | ||
|
|
c340544152 | ||
|
|
00c7cae923 | ||
|
|
80cff6912b | ||
|
|
b76e1d4167 | ||
|
|
f7759d2547 | ||
|
|
d51e3979ff | ||
|
|
01275fce9f | ||
|
|
261b18f4eb | ||
|
|
fdc20aa84f | ||
|
|
a3382c2d5b | ||
|
|
52a9a2f018 | ||
|
|
cfea1c16bc | ||
|
|
180c07cf61 | ||
|
|
f880db4902 |
+1
-1
@@ -2,8 +2,8 @@
|
|||||||
<configuration default="false" name="Cryptomator macOS" type="Application" factoryName="Application">
|
<configuration default="false" name="Cryptomator macOS" type="Application" factoryName="Application">
|
||||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||||
<module name="launcher" />
|
<module name="launcher" />
|
||||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator/settings.json" -Dcryptomator.ipcPortPath="~/Library/Application Support/Cryptomator/ipcPort.bin" -Dcryptomator.logDir="~/Library/Logs/Cryptomator" -Dcryptomator.mountPointsDir="/Volumes/" -Xss2m -Xmx512m" />
|
|
||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||||
|
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator/settings.json" -Dcryptomator.ipcPortPath="~/Library/Application Support/Cryptomator/ipcPort.bin" -Dcryptomator.logDir="~/Library/Logs/Cryptomator" -Dcryptomator.mountPointsDir="/Volumes/" -Xss2m -Xmx512m -ea" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
@@ -14,6 +14,13 @@
|
|||||||
</includes>
|
</includes>
|
||||||
<outputDirectory>libs</outputDirectory>
|
<outputDirectory>libs</outputDirectory>
|
||||||
</fileSet>
|
</fileSet>
|
||||||
|
<fileSet>
|
||||||
|
<directory>target/</directory>
|
||||||
|
<includes>
|
||||||
|
<include>ffi-version.txt</include>
|
||||||
|
</includes>
|
||||||
|
<outputDirectory>libs</outputDirectory>
|
||||||
|
</fileSet>
|
||||||
<fileSet>
|
<fileSet>
|
||||||
<directory>target/</directory>
|
<directory>target/</directory>
|
||||||
<includes>
|
<includes>
|
||||||
|
|||||||
@@ -14,6 +14,13 @@
|
|||||||
</includes>
|
</includes>
|
||||||
<outputDirectory>libs</outputDirectory>
|
<outputDirectory>libs</outputDirectory>
|
||||||
</fileSet>
|
</fileSet>
|
||||||
|
<fileSet>
|
||||||
|
<directory>target/</directory>
|
||||||
|
<includes>
|
||||||
|
<include>ffi-version.txt</include>
|
||||||
|
</includes>
|
||||||
|
<outputDirectory>libs</outputDirectory>
|
||||||
|
</fileSet>
|
||||||
<fileSet>
|
<fileSet>
|
||||||
<directory>target/</directory>
|
<directory>target/</directory>
|
||||||
<includes>
|
<includes>
|
||||||
|
|||||||
@@ -14,6 +14,13 @@
|
|||||||
</includes>
|
</includes>
|
||||||
<outputDirectory>libs</outputDirectory>
|
<outputDirectory>libs</outputDirectory>
|
||||||
</fileSet>
|
</fileSet>
|
||||||
|
<fileSet>
|
||||||
|
<directory>target/</directory>
|
||||||
|
<includes>
|
||||||
|
<include>ffi-version.txt</include>
|
||||||
|
</includes>
|
||||||
|
<outputDirectory>libs</outputDirectory>
|
||||||
|
</fileSet>
|
||||||
<fileSet>
|
<fileSet>
|
||||||
<directory>target/</directory>
|
<directory>target/</directory>
|
||||||
<includes>
|
<includes>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.4.16</version>
|
<version>1.5.0-alpha1</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>buildkit</artifactId>
|
<artifactId>buildkit</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
<directory>${project.basedir}/src/main/resources</directory>
|
<directory>${project.basedir}/src/main/resources</directory>
|
||||||
<includes>
|
<includes>
|
||||||
<include>version.txt</include>
|
<include>version.txt</include>
|
||||||
|
<include>ffi-version.txt</include>
|
||||||
<include>launcher-mac.sh</include>
|
<include>launcher-mac.sh</include>
|
||||||
<include>launcher-linux.sh</include>
|
<include>launcher-linux.sh</include>
|
||||||
<include>launcher-win.bat</include>
|
<include>launcher-win.bat</include>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
${cryptomator.jni.version}
|
||||||
+37
-8
@@ -4,35 +4,64 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.4.16</version>
|
<version>1.5.0-alpha1</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>commons</artifactId>
|
<artifactId>commons</artifactId>
|
||||||
<name>Cryptomator Commons</name>
|
<name>Cryptomator Commons</name>
|
||||||
<description>Shared utilities</description>
|
<description>Shared utilities</description>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.cryptomator</groupId>
|
||||||
|
<artifactId>cryptofs</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.cryptomator</groupId>
|
||||||
|
<artifactId>fuse-nio-adapter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.cryptomator</groupId>
|
||||||
|
<artifactId>dokany-nio-adapter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.cryptomator</groupId>
|
||||||
|
<artifactId>webdav-nio-adapter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.cryptomator</groupId>
|
||||||
|
<artifactId>jni</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- JavaFx -->
|
<!-- JavaFx -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-base</artifactId>
|
<artifactId>javafx-base</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-graphics</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Libs -->
|
<!-- EasyBind -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.fxmisc.easybind</groupId>
|
||||||
|
<artifactId>easybind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Google -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.guava</groupId>
|
<groupId>com.google.guava</groupId>
|
||||||
<artifactId>guava</artifactId>
|
<artifactId>guava</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.commons</groupId>
|
|
||||||
<artifactId>commons-lang3</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.code.gson</groupId>
|
<groupId>com.google.code.gson</groupId>
|
||||||
<artifactId>gson</artifactId>
|
<artifactId>gson</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Apache Commons -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.fxmisc.easybind</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>easybind</artifactId>
|
<artifactId>commons-lang3</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- DI -->
|
<!-- DI -->
|
||||||
|
|||||||
@@ -5,22 +5,89 @@
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.common;
|
package org.cryptomator.common;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import dagger.Binds;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import javafx.beans.binding.Binding;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
|
import org.cryptomator.common.settings.Settings;
|
||||||
|
import org.cryptomator.common.settings.SettingsProvider;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.common.vaults.VaultComponent;
|
||||||
|
import org.cryptomator.common.vaults.VaultListManager;
|
||||||
|
import org.cryptomator.frontend.webdav.WebDavServer;
|
||||||
|
import org.fxmisc.easybind.EasyBind;
|
||||||
|
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import dagger.Module;
|
@Module(subcomponents = {VaultComponent.class})
|
||||||
import dagger.Provides;
|
public abstract class CommonsModule {
|
||||||
|
|
||||||
@Module
|
private static final int NUM_SCHEDULER_THREADS = 4;
|
||||||
public class CommonsModule {
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@Named("SemVer")
|
@Named("SemVer")
|
||||||
Comparator<String> providesSemVerComparator() {
|
static Comparator<String> providesSemVerComparator() {
|
||||||
return new SemVerComparator();
|
return new SemVerComparator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
static Settings provideSettings(SettingsProvider settingsProvider) {
|
||||||
|
return settingsProvider.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
static ObservableList<Vault> provideVaultList(VaultListManager vaultListManager) {
|
||||||
|
return vaultListManager.getVaultList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
static ScheduledExecutorService provideScheduledExecutorService(@Named("shutdownTaskScheduler") Consumer<Runnable> shutdownTaskScheduler) {
|
||||||
|
final AtomicInteger threadNumber = new AtomicInteger(1);
|
||||||
|
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(NUM_SCHEDULER_THREADS, r -> {
|
||||||
|
Thread t = new Thread(r);
|
||||||
|
t.setName("Background Thread " + threadNumber.getAndIncrement());
|
||||||
|
t.setDaemon(true);
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
shutdownTaskScheduler.accept(executorService::shutdown);
|
||||||
|
return executorService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract ExecutorService bindExecutorService(ScheduledExecutorService executor);
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
static Binding<InetSocketAddress> provideServerSocketAddressBinding(Settings settings) {
|
||||||
|
return Bindings.createObjectBinding(() -> {
|
||||||
|
String host = SystemUtils.IS_OS_WINDOWS ? "127.0.0.1" : "localhost";
|
||||||
|
return InetSocketAddress.createUnresolved(host, settings.port().intValue());
|
||||||
|
}, settings.port());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
static WebDavServer provideWebDavServer(Binding<InetSocketAddress> serverSocketAddressBinding) {
|
||||||
|
WebDavServer server = WebDavServer.create();
|
||||||
|
// no need to unsubscribe eventually, because server is a singleton
|
||||||
|
EasyBind.subscribe(serverSocketAddressBinding, server::bind);
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public class Environment {
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public Environment() {
|
public Environment() {
|
||||||
|
LOG.debug("java.library.path: {}", System.getProperty("java.library.path"));
|
||||||
LOG.debug("user.language: {}", System.getProperty("user.language"));
|
LOG.debug("user.language: {}", System.getProperty("user.language"));
|
||||||
LOG.debug("user.region: {}", System.getProperty("user.region"));
|
LOG.debug("user.region: {}", System.getProperty("user.region"));
|
||||||
LOG.debug("logback.configurationFile: {}", System.getProperty("logback.configurationFile"));
|
LOG.debug("logback.configurationFile: {}", System.getProperty("logback.configurationFile"));
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (c) 2019 Skymatic GmbH.
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the accompanying LICENSE file.
|
||||||
|
*******************************************************************************/
|
||||||
|
package org.cryptomator.common;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import org.cryptomator.jni.JniFunctions;
|
||||||
|
import org.cryptomator.jni.MacFunctions;
|
||||||
|
import org.cryptomator.jni.WinFunctions;
|
||||||
|
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
public class JniModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
Optional<MacFunctions> provideOptionalMacFunctions() {
|
||||||
|
return JniFunctions.macFunctions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
Optional<WinFunctions> provideOptionalWinFunctions() {
|
||||||
|
return JniFunctions.winFunctions();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,10 +8,14 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package org.cryptomator.common.settings;
|
package org.cryptomator.common.settings;
|
||||||
|
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.Observable;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ListChangeListener;
|
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
@@ -22,20 +26,24 @@ public class Settings {
|
|||||||
public static final int MAX_PORT = 65535;
|
public static final int MAX_PORT = 65535;
|
||||||
public static final boolean DEFAULT_ASKED_FOR_UPDATE_CHECK = false;
|
public static final boolean DEFAULT_ASKED_FOR_UPDATE_CHECK = false;
|
||||||
public static final boolean DEFAULT_CHECK_FOR_UDPATES = false;
|
public static final boolean DEFAULT_CHECK_FOR_UDPATES = false;
|
||||||
|
public static final boolean DEFAULT_START_HIDDEN = false;
|
||||||
public static final int DEFAULT_PORT = 42427;
|
public static final int DEFAULT_PORT = 42427;
|
||||||
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
|
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
|
||||||
public static final String DEFAULT_GVFS_SCHEME = "dav";
|
public static final WebDavUrlScheme DEFAULT_GVFS_SCHEME = WebDavUrlScheme.DAV;
|
||||||
|
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
|
||||||
public static final boolean DEFAULT_DEBUG_MODE = false;
|
public static final boolean DEFAULT_DEBUG_MODE = false;
|
||||||
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = System.getProperty("os.name").toLowerCase().contains("windows") ? VolumeImpl.DOKANY : VolumeImpl.FUSE;
|
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = System.getProperty("os.name").toLowerCase().contains("windows") ? VolumeImpl.DOKANY : VolumeImpl.FUSE;
|
||||||
|
|
||||||
private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList(VaultSettings::observables);
|
private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList(VaultSettings::observables);
|
||||||
private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK);
|
private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK);
|
||||||
private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UDPATES);
|
private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UDPATES);
|
||||||
|
private final BooleanProperty startHidden = new SimpleBooleanProperty(DEFAULT_START_HIDDEN);
|
||||||
private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT);
|
private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT);
|
||||||
private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS);
|
private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS);
|
||||||
private final StringProperty preferredGvfsScheme = new SimpleStringProperty(DEFAULT_GVFS_SCHEME);
|
private final ObjectProperty<WebDavUrlScheme> preferredGvfsScheme = new SimpleObjectProperty<>(DEFAULT_GVFS_SCHEME);
|
||||||
private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE);
|
private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE);
|
||||||
private final ObjectProperty<VolumeImpl> preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL);
|
private final ObjectProperty<VolumeImpl> preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL);
|
||||||
|
private final ObjectProperty<UiTheme> theme = new SimpleObjectProperty<>(DEFAULT_THEME);
|
||||||
|
|
||||||
private Consumer<Settings> saveCmd;
|
private Consumer<Settings> saveCmd;
|
||||||
|
|
||||||
@@ -43,21 +51,23 @@ public class Settings {
|
|||||||
* Package-private constructor; use {@link SettingsProvider}.
|
* Package-private constructor; use {@link SettingsProvider}.
|
||||||
*/
|
*/
|
||||||
Settings() {
|
Settings() {
|
||||||
directories.addListener((ListChangeListener.Change<? extends VaultSettings> change) -> this.save());
|
directories.addListener(this::somethingChanged);
|
||||||
askedForUpdateCheck.addListener(this::somethingChanged);
|
askedForUpdateCheck.addListener(this::somethingChanged);
|
||||||
checkForUpdates.addListener(this::somethingChanged);
|
checkForUpdates.addListener(this::somethingChanged);
|
||||||
|
startHidden.addListener(this::somethingChanged);
|
||||||
port.addListener(this::somethingChanged);
|
port.addListener(this::somethingChanged);
|
||||||
numTrayNotifications.addListener(this::somethingChanged);
|
numTrayNotifications.addListener(this::somethingChanged);
|
||||||
preferredGvfsScheme.addListener(this::somethingChanged);
|
preferredGvfsScheme.addListener(this::somethingChanged);
|
||||||
debugMode.addListener(this::somethingChanged);
|
debugMode.addListener(this::somethingChanged);
|
||||||
preferredVolumeImpl.addListener(this::somethingChanged);
|
preferredVolumeImpl.addListener(this::somethingChanged);
|
||||||
|
theme.addListener(this::somethingChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setSaveCmd(Consumer<Settings> saveCmd) {
|
void setSaveCmd(Consumer<Settings> saveCmd) {
|
||||||
this.saveCmd = saveCmd;
|
this.saveCmd = saveCmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void somethingChanged(ObservableValue<?> observable, Object oldValue, Object newValue) {
|
private void somethingChanged(@SuppressWarnings("unused") Observable observable) {
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +90,10 @@ public class Settings {
|
|||||||
public BooleanProperty checkForUpdates() {
|
public BooleanProperty checkForUpdates() {
|
||||||
return checkForUpdates;
|
return checkForUpdates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BooleanProperty startHidden() {
|
||||||
|
return startHidden;
|
||||||
|
}
|
||||||
|
|
||||||
public IntegerProperty port() {
|
public IntegerProperty port() {
|
||||||
return port;
|
return port;
|
||||||
@@ -89,7 +103,7 @@ public class Settings {
|
|||||||
return numTrayNotifications;
|
return numTrayNotifications;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringProperty preferredGvfsScheme() {
|
public ObjectProperty<WebDavUrlScheme> preferredGvfsScheme() {
|
||||||
return preferredGvfsScheme;
|
return preferredGvfsScheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,4 +115,7 @@ public class Settings {
|
|||||||
return preferredVolumeImpl;
|
return preferredVolumeImpl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<UiTheme> theme() {
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-3
@@ -29,11 +29,13 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
|||||||
writeVaultSettingsArray(out, value.getDirectories());
|
writeVaultSettingsArray(out, value.getDirectories());
|
||||||
out.name("askedForUpdateCheck").value(value.askedForUpdateCheck().get());
|
out.name("askedForUpdateCheck").value(value.askedForUpdateCheck().get());
|
||||||
out.name("checkForUpdatesEnabled").value(value.checkForUpdates().get());
|
out.name("checkForUpdatesEnabled").value(value.checkForUpdates().get());
|
||||||
|
out.name("startHidden").value(value.startHidden().get());
|
||||||
out.name("port").value(value.port().get());
|
out.name("port").value(value.port().get());
|
||||||
out.name("numTrayNotifications").value(value.numTrayNotifications().get());
|
out.name("numTrayNotifications").value(value.numTrayNotifications().get());
|
||||||
out.name("preferredGvfsScheme").value(value.preferredGvfsScheme().get());
|
out.name("preferredGvfsScheme").value(value.preferredGvfsScheme().get().name());
|
||||||
out.name("debugMode").value(value.debugMode().get());
|
out.name("debugMode").value(value.debugMode().get());
|
||||||
out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name());
|
out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name());
|
||||||
|
out.name("theme").value(value.theme().get().name());
|
||||||
out.endObject();
|
out.endObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +64,9 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
|||||||
case "checkForUpdatesEnabled":
|
case "checkForUpdatesEnabled":
|
||||||
settings.checkForUpdates().set(in.nextBoolean());
|
settings.checkForUpdates().set(in.nextBoolean());
|
||||||
break;
|
break;
|
||||||
|
case "startHidden":
|
||||||
|
settings.startHidden().set(in.nextBoolean());
|
||||||
|
break;
|
||||||
case "port":
|
case "port":
|
||||||
settings.port().set(in.nextInt());
|
settings.port().set(in.nextInt());
|
||||||
break;
|
break;
|
||||||
@@ -69,7 +74,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
|||||||
settings.numTrayNotifications().set(in.nextInt());
|
settings.numTrayNotifications().set(in.nextInt());
|
||||||
break;
|
break;
|
||||||
case "preferredGvfsScheme":
|
case "preferredGvfsScheme":
|
||||||
settings.preferredGvfsScheme().set(in.nextString());
|
settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
|
||||||
break;
|
break;
|
||||||
case "debugMode":
|
case "debugMode":
|
||||||
settings.debugMode().set(in.nextBoolean());
|
settings.debugMode().set(in.nextBoolean());
|
||||||
@@ -77,6 +82,9 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
|||||||
case "preferredVolumeImpl":
|
case "preferredVolumeImpl":
|
||||||
settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
|
settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
|
||||||
break;
|
break;
|
||||||
|
case "theme":
|
||||||
|
settings.theme().set(parseUiTheme(in.nextString()));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
LOG.warn("Unsupported vault setting found in JSON: " + name);
|
LOG.warn("Unsupported vault setting found in JSON: " + name);
|
||||||
in.skipValue();
|
in.skipValue();
|
||||||
@@ -90,12 +98,31 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
|||||||
|
|
||||||
private VolumeImpl parsePreferredVolumeImplName(String nioAdapterName) {
|
private VolumeImpl parsePreferredVolumeImplName(String nioAdapterName) {
|
||||||
try {
|
try {
|
||||||
return VolumeImpl.valueOf(nioAdapterName);
|
return VolumeImpl.valueOf(nioAdapterName.toUpperCase());
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
|
LOG.warn("Invalid volume type {}. Defaulting to {}.", nioAdapterName, Settings.DEFAULT_PREFERRED_VOLUME_IMPL);
|
||||||
return Settings.DEFAULT_PREFERRED_VOLUME_IMPL;
|
return Settings.DEFAULT_PREFERRED_VOLUME_IMPL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private WebDavUrlScheme parseWebDavUrlSchemePrefix(String webDavUrlSchemeName) {
|
||||||
|
try {
|
||||||
|
return WebDavUrlScheme.valueOf(webDavUrlSchemeName.toUpperCase());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
LOG.warn("Invalid volume type {}. Defaulting to {}.", webDavUrlSchemeName, Settings.DEFAULT_GVFS_SCHEME);
|
||||||
|
return Settings.DEFAULT_GVFS_SCHEME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UiTheme parseUiTheme(String uiThemeName) {
|
||||||
|
try {
|
||||||
|
return UiTheme.valueOf(uiThemeName.toUpperCase());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
LOG.warn("Invalid volume type {}. Defaulting to {}.", uiThemeName, Settings.DEFAULT_THEME);
|
||||||
|
return Settings.DEFAULT_THEME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<VaultSettings> readVaultSettingsArray(JsonReader in) throws IOException {
|
private List<VaultSettings> readVaultSettingsArray(JsonReader in) throws IOException {
|
||||||
List<VaultSettings> result = new ArrayList<>();
|
List<VaultSettings> result = new ArrayList<>();
|
||||||
in.beginArray();
|
in.beginArray();
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Provider;
|
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -38,10 +37,11 @@ import java.util.concurrent.ScheduledExecutorService;
|
|||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class SettingsProvider implements Provider<Settings> {
|
public class SettingsProvider implements Supplier<Settings> {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class);
|
private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class);
|
||||||
private static final long SAVE_DELAY_MS = 1000;
|
private static final long SAVE_DELAY_MS = 1000;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package org.cryptomator.common.settings;
|
||||||
|
|
||||||
|
public enum UiTheme {
|
||||||
|
LIGHT("Light"),
|
||||||
|
DARK("Dark");
|
||||||
|
// CUSTOM("Custom (%s)");
|
||||||
|
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
|
UiTheme(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
package org.cryptomator.common.settings;
|
package org.cryptomator.common.settings;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.io.BaseEncoding;
|
||||||
import javafx.beans.Observable;
|
import javafx.beans.Observable;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
@@ -23,6 +24,7 @@ import java.util.Base64;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,6 +38,8 @@ public class VaultSettings {
|
|||||||
public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
|
public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
|
||||||
public static final boolean DEFAULT_USES_READONLY_MODE = false;
|
public static final boolean DEFAULT_USES_READONLY_MODE = false;
|
||||||
public static final String DEFAULT_MOUNT_FLAGS = "";
|
public static final String DEFAULT_MOUNT_FLAGS = "";
|
||||||
|
|
||||||
|
private static final Random RNG = new Random();
|
||||||
|
|
||||||
private final String id;
|
private final String id;
|
||||||
private final ObjectProperty<Path> path = new SimpleObjectProperty();
|
private final ObjectProperty<Path> path = new SimpleObjectProperty();
|
||||||
@@ -69,20 +73,9 @@ public class VaultSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String generateId() {
|
private static String generateId() {
|
||||||
return asBase64String(nineBytesFrom(UUID.randomUUID()));
|
byte[] randomBytes = new byte[9];
|
||||||
}
|
RNG.nextBytes(randomBytes);
|
||||||
|
return BaseEncoding.base64Url().encode(randomBytes);
|
||||||
private static String asBase64String(byte[] bytes) {
|
|
||||||
byte[] base64Bytes = Base64.getUrlEncoder().encode(bytes);
|
|
||||||
return new String(base64Bytes, StandardCharsets.US_ASCII);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] nineBytesFrom(UUID uuid) {
|
|
||||||
ByteBuffer uuidBuffer = ByteBuffer.allocate(9);
|
|
||||||
uuidBuffer.putLong(uuid.getMostSignificantBits());
|
|
||||||
uuidBuffer.put((byte) (uuid.getLeastSignificantBits() & 0xFF));
|
|
||||||
uuidBuffer.flip();
|
|
||||||
return uuidBuffer.array();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String normalizeMountName(String mountName) {
|
public static String normalizeMountName(String mountName) {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package org.cryptomator.common.settings;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public enum WebDavUrlScheme {
|
||||||
|
DAV("dav", "dav:// (Gnome, Nautilus, ...)"),
|
||||||
|
WEBDAV("webdav", "webdav:// (KDE, Dolphin, ...)");
|
||||||
|
|
||||||
|
private final String prefix;
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
WebDavUrlScheme(String prefix, String displayName) {this.prefix = prefix;
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPrefix() {
|
||||||
|
return prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a WebDavUrlScheme by prefix.
|
||||||
|
*
|
||||||
|
* @param prefix Prefix of the WebDavUrlScheme
|
||||||
|
* @return WebDavUrlScheme with the given <code>prefix</code>.
|
||||||
|
* @throws IllegalArgumentException if not WebDavUrlScheme with the given <code>prefix</code> was found.
|
||||||
|
*/
|
||||||
|
public static WebDavUrlScheme forPrefix(String prefix) throws IllegalArgumentException {
|
||||||
|
return Arrays.stream(values()) //
|
||||||
|
.filter(impl -> impl.prefix.equals(prefix)) //
|
||||||
|
.findAny() //
|
||||||
|
.orElseThrow(IllegalArgumentException::new);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package org.cryptomator.ui.model;
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
import javax.inject.Qualifier;
|
import javax.inject.Qualifier;
|
||||||
import java.lang.annotation.Documented;
|
import java.lang.annotation.Documented;
|
||||||
+11
-5
@@ -1,4 +1,4 @@
|
|||||||
package org.cryptomator.ui.model;
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
import org.cryptomator.common.settings.VaultSettings;
|
||||||
@@ -30,6 +30,7 @@ public class DokanyVolume implements Volume {
|
|||||||
private final MountFactory mountFactory;
|
private final MountFactory mountFactory;
|
||||||
private final WindowsDriveLetters windowsDriveLetters;
|
private final WindowsDriveLetters windowsDriveLetters;
|
||||||
private Mount mount;
|
private Mount mount;
|
||||||
|
private Path mountPoint;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, WindowsDriveLetters windowsDriveLetters) {
|
public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, WindowsDriveLetters windowsDriveLetters) {
|
||||||
@@ -45,19 +46,19 @@ public class DokanyVolume implements Volume {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException, IOException {
|
public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException, IOException {
|
||||||
Path mountPath = getMountPoint();
|
this.mountPoint = determineMountPoint();
|
||||||
String mountName = vaultSettings.mountName().get();
|
String mountName = vaultSettings.mountName().get();
|
||||||
try {
|
try {
|
||||||
this.mount = mountFactory.mount(fs.getPath("/"), mountPath, mountName, FS_TYPE_NAME, mountFlags.strip());
|
this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, mountName, FS_TYPE_NAME, mountFlags.strip());
|
||||||
} catch (MountFailedException e) {
|
} catch (MountFailedException e) {
|
||||||
if (vaultSettings.getIndividualMountPath().isPresent()) {
|
if (vaultSettings.getIndividualMountPath().isPresent()) {
|
||||||
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPath);
|
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
|
||||||
}
|
}
|
||||||
throw new VolumeException("Unable to mount Filesystem", e);
|
throw new VolumeException("Unable to mount Filesystem", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path getMountPoint() throws VolumeException, IOException {
|
private Path determineMountPoint() throws VolumeException, IOException {
|
||||||
Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
|
Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
|
||||||
if (optionalCustomMountPoint.isPresent()) {
|
if (optionalCustomMountPoint.isPresent()) {
|
||||||
Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
|
Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
|
||||||
@@ -99,6 +100,11 @@ public class DokanyVolume implements Volume {
|
|||||||
mount.close();
|
mount.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Path> getMountPoint() {
|
||||||
|
return Optional.ofNullable(mountPoint);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isSupportedStatic() {
|
public static boolean isSupportedStatic() {
|
||||||
return MountFactory.isApplicable();
|
return MountFactory.isApplicable();
|
||||||
}
|
}
|
||||||
+7
-3
@@ -1,4 +1,4 @@
|
|||||||
package org.cryptomator.ui.model;
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
import com.google.common.base.Splitter;
|
import com.google.common.base.Splitter;
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
@@ -100,8 +100,7 @@ public class FuseVolume implements Volume {
|
|||||||
try {
|
try {
|
||||||
Mounter mounter = FuseMountFactory.getMounter();
|
Mounter mounter = FuseMountFactory.getMounter();
|
||||||
EnvironmentVariables envVars = EnvironmentVariables.create() //
|
EnvironmentVariables envVars = EnvironmentVariables.create() //
|
||||||
.withFlags(splitFlags(mountFlags))
|
.withFlags(splitFlags(mountFlags)).withMountPoint(mountPoint) //
|
||||||
.withMountPoint(mountPoint) //
|
|
||||||
.build();
|
.build();
|
||||||
this.fuseMnt = mounter.mount(root, envVars);
|
this.fuseMnt = mounter.mount(root, envVars);
|
||||||
} catch (CommandFailedException e) {
|
} catch (CommandFailedException e) {
|
||||||
@@ -166,6 +165,11 @@ public class FuseVolume implements Volume {
|
|||||||
return FuseVolume.isSupportedStatic();
|
return FuseVolume.isSupportedStatic();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Path> getMountPoint() {
|
||||||
|
return Optional.ofNullable(mountPoint);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isSupportedStatic() {
|
public static boolean isSupportedStatic() {
|
||||||
return (SystemUtils.IS_OS_MAC_OSX || SystemUtils.IS_OS_LINUX) && FuseMountFactory.isFuseSupported();
|
return (SystemUtils.IS_OS_MAC_OSX || SystemUtils.IS_OS_LINUX) && FuseMountFactory.isFuseSupported();
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package org.cryptomator.ui.model;
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
import javax.inject.Scope;
|
import javax.inject.Scope;
|
||||||
import java.lang.annotation.Documented;
|
import java.lang.annotation.Documented;
|
||||||
+136
-165
@@ -6,14 +6,16 @@
|
|||||||
* Contributors:
|
* Contributors:
|
||||||
* Sebastian Stenzel - initial API and implementation
|
* Sebastian Stenzel - initial API and implementation
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.ui.model;
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.beans.Observable;
|
import javafx.beans.Observable;
|
||||||
import javafx.beans.binding.Binding;
|
import javafx.beans.binding.Binding;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.binding.ObjectBinding;
|
||||||
|
import javafx.beans.binding.StringBinding;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
@@ -39,36 +41,51 @@ import java.nio.file.NotDirectoryException;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
@PerVault
|
@PerVault
|
||||||
public class Vault {
|
public class Vault {
|
||||||
|
|
||||||
public static final Predicate<Vault> NOT_LOCKED = hasState(State.LOCKED).negate();
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
|
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
|
||||||
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
|
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
|
||||||
|
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
|
||||||
|
|
||||||
private final VaultSettings vaultSettings;
|
private final VaultSettings vaultSettings;
|
||||||
private final Provider<Volume> volumeProvider;
|
private final Provider<Volume> volumeProvider;
|
||||||
private final Supplier<String> defaultMountFlags;
|
private final StringBinding defaultMountFlags;
|
||||||
private final AtomicReference<CryptoFileSystem> cryptoFileSystem = new AtomicReference<>();
|
private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
|
||||||
private final ObjectProperty<State> state = new SimpleObjectProperty<State>(State.LOCKED);
|
private final ObjectProperty<VaultState> state;
|
||||||
|
private final VaultStats stats;
|
||||||
|
private final StringBinding displayableName;
|
||||||
|
private final StringBinding displayablePath;
|
||||||
|
private final BooleanBinding locked;
|
||||||
|
private final BooleanBinding processing;
|
||||||
|
private final BooleanBinding unlocked;
|
||||||
|
private final BooleanBinding needsMigration;
|
||||||
|
private final ObjectBinding<Path> accessPoint;
|
||||||
|
|
||||||
private Volume volume;
|
private volatile Volume volume;
|
||||||
|
|
||||||
public enum State {
|
|
||||||
LOCKED, PROCESSING, UNLOCKED
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags Supplier<String> defaultMountFlags) {
|
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, ObjectProperty<VaultState> state, VaultStats stats) {
|
||||||
this.vaultSettings = vaultSettings;
|
this.vaultSettings = vaultSettings;
|
||||||
this.volumeProvider = volumeProvider;
|
this.volumeProvider = volumeProvider;
|
||||||
this.defaultMountFlags = defaultMountFlags;
|
this.defaultMountFlags = defaultMountFlags;
|
||||||
|
this.cryptoFileSystem = cryptoFileSystem;
|
||||||
|
this.state = state;
|
||||||
|
this.stats = stats;
|
||||||
|
this.displayableName = Bindings.createStringBinding(this::getDisplayableName, vaultSettings.path());
|
||||||
|
this.displayablePath = Bindings.createStringBinding(this::getDisplayablePath, vaultSettings.path());
|
||||||
|
this.locked = Bindings.createBooleanBinding(this::isLocked, state);
|
||||||
|
this.processing = Bindings.createBooleanBinding(this::isProcessing, state);
|
||||||
|
this.unlocked = Bindings.createBooleanBinding(this::isUnlocked, state);
|
||||||
|
this.needsMigration = Bindings.createBooleanBinding(this::isNeedsMigration, state);
|
||||||
|
this.accessPoint = Bindings.createObjectBinding(this::getAccessPoint, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ******************************************************************************
|
// ******************************************************************************
|
||||||
@@ -80,7 +97,7 @@ public class Vault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private CryptoFileSystem unlockCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
|
private CryptoFileSystem unlockCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
|
||||||
List<FileSystemFlags> flags = new ArrayList<>();
|
Set<FileSystemFlags> flags = EnumSet.noneOf(FileSystemFlags.class);
|
||||||
if (vaultSettings.usesReadOnlyMode().get()) {
|
if (vaultSettings.usesReadOnlyMode().get()) {
|
||||||
flags.add(FileSystemFlags.READONLY);
|
flags.add(FileSystemFlags.READONLY);
|
||||||
}
|
}
|
||||||
@@ -92,40 +109,16 @@ public class Vault {
|
|||||||
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
|
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void create(CharSequence passphrase) throws IOException {
|
|
||||||
if (!isValidVaultDirectory()) {
|
|
||||||
CryptoFileSystemProvider.initialize(getPath(), MASTERKEY_FILENAME, passphrase);
|
|
||||||
} else {
|
|
||||||
throw new FileAlreadyExistsException(getPath().toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void changePassphrase(CharSequence oldPassphrase, CharSequence newPassphrase) throws IOException, InvalidPassphraseException {
|
|
||||||
CryptoFileSystemProvider.changePassphrase(getPath(), MASTERKEY_FILENAME, oldPassphrase, newPassphrase);
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, Volume.VolumeException {
|
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, Volume.VolumeException {
|
||||||
Platform.runLater(() -> state.set(State.PROCESSING));
|
if (vaultSettings.usesIndividualMountPath().get() && Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) {
|
||||||
try {
|
throw new NotDirectoryException("");
|
||||||
if (vaultSettings.usesIndividualMountPath().get() && Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) {
|
|
||||||
throw new NotDirectoryException("");
|
|
||||||
}
|
|
||||||
CryptoFileSystem fs = getCryptoFileSystem(passphrase);
|
|
||||||
volume = volumeProvider.get();
|
|
||||||
volume.mount(fs, getMountFlags());
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
state.set(State.UNLOCKED);
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
Platform.runLater(() -> state.set(State.LOCKED));
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
CryptoFileSystem fs = getCryptoFileSystem(passphrase);
|
||||||
|
volume = volumeProvider.get();
|
||||||
|
volume.mount(fs, getEffectiveMountFlags());
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void lock(boolean forced) throws Volume.VolumeException {
|
public synchronized void lock(boolean forced) throws Volume.VolumeException {
|
||||||
Platform.runLater(() -> {
|
|
||||||
state.set(State.PROCESSING);
|
|
||||||
});
|
|
||||||
if (forced && volume.supportsForcedUnmount()) {
|
if (forced && volume.supportsForcedUnmount()) {
|
||||||
volume.unmountForced();
|
volume.unmountForced();
|
||||||
} else {
|
} else {
|
||||||
@@ -139,28 +132,6 @@ public class Vault {
|
|||||||
LOG.error("Error closing file system.", e);
|
LOG.error("Error closing file system.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Platform.runLater(() -> {
|
|
||||||
state.set(State.LOCKED);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ejects any mounted drives and locks this vault. no-op if this vault is currently locked.
|
|
||||||
*/
|
|
||||||
public void prepareForShutdown() {
|
|
||||||
try {
|
|
||||||
lock(false);
|
|
||||||
} catch (Volume.VolumeException e) {
|
|
||||||
if (volume.supportsForcedUnmount()) {
|
|
||||||
try {
|
|
||||||
lock(true);
|
|
||||||
} catch (Volume.VolumeException e1) {
|
|
||||||
LOG.warn("Failed to force lock vault.", e1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG.warn("Failed to gracefully lock vault.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reveal() throws Volume.VolumeException {
|
public void reveal() throws Volume.VolumeException {
|
||||||
@@ -168,21 +139,96 @@ public class Vault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ******************************************************************************
|
// ******************************************************************************
|
||||||
// Getter/Setter
|
// Observable Properties
|
||||||
// *******************************************************************************/
|
// *******************************************************************************
|
||||||
|
|
||||||
public State getState() {
|
public ObjectProperty<VaultState> stateProperty() {
|
||||||
return state.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Predicate<Vault> hasState(State state) {
|
public VaultState getState() {
|
||||||
return vault -> {
|
return state.get();
|
||||||
return vault.getState() == state;
|
}
|
||||||
};
|
|
||||||
|
public void setState(VaultState value) {
|
||||||
|
state.setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding lockedProperty() {
|
||||||
|
return locked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLocked() {
|
||||||
|
return state.get() == VaultState.LOCKED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding processingProperty() {
|
||||||
|
return processing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isProcessing() {
|
||||||
|
return state.get() == VaultState.PROCESSING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding unlockedProperty() {
|
||||||
|
return unlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUnlocked() {
|
||||||
|
return state.get() == VaultState.UNLOCKED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding needsMigrationProperty() {
|
||||||
|
return needsMigration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNeedsMigration() {
|
||||||
|
return state.get() == VaultState.NEEDS_MIGRATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringBinding displayableNameProperty() {
|
||||||
|
return displayableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayableName() {
|
||||||
|
Path p = vaultSettings.path().get();
|
||||||
|
return p.getFileName().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectBinding<Path> accessPointProperty() {
|
||||||
|
return accessPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getAccessPoint() {
|
||||||
|
if (state.get() == VaultState.UNLOCKED) {
|
||||||
|
assert volume != null;
|
||||||
|
return volume.getMountPoint().orElse(Path.of(""));
|
||||||
|
} else {
|
||||||
|
return Path.of("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringBinding displayablePathProperty() {
|
||||||
|
return displayablePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayablePath() {
|
||||||
|
Path p = vaultSettings.path().get();
|
||||||
|
if (p.startsWith(HOME_DIR)) {
|
||||||
|
Path relativePath = HOME_DIR.relativize(p);
|
||||||
|
String homePrefix = SystemUtils.IS_OS_WINDOWS ? "~\\" : "~/";
|
||||||
|
return homePrefix + relativePath.toString();
|
||||||
|
} else {
|
||||||
|
return p.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ******************************************************************************
|
||||||
|
// Getter/Setter
|
||||||
|
// *******************************************************************************/
|
||||||
|
|
||||||
|
public VaultStats getStats() {
|
||||||
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Observable[] observables() {
|
public Observable[] observables() {
|
||||||
@@ -197,106 +243,31 @@ public class Vault {
|
|||||||
return vaultSettings.path().getValue();
|
return vaultSettings.path().getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Binding<String> displayablePath() {
|
|
||||||
Path homeDir = Paths.get(SystemUtils.USER_HOME);
|
|
||||||
return EasyBind.map(vaultSettings.path(), p -> {
|
|
||||||
if (p.startsWith(homeDir)) {
|
|
||||||
Path relativePath = homeDir.relativize(p);
|
|
||||||
String homePrefix = SystemUtils.IS_OS_WINDOWS ? "~\\" : "~/";
|
|
||||||
return homePrefix + relativePath.toString();
|
|
||||||
} else {
|
|
||||||
return p.toString();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Directory name without preceeding path components and file extension
|
|
||||||
*/
|
|
||||||
public Binding<String> name() {
|
|
||||||
return EasyBind.map(vaultSettings.path(), Path::getFileName).map(Path::toString);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean doesVaultDirectoryExist() {
|
|
||||||
return Files.isDirectory(getPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isValidVaultDirectory() {
|
|
||||||
return CryptoFileSystemProvider.containsVault(getPath(), MASTERKEY_FILENAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long pollBytesRead() {
|
|
||||||
CryptoFileSystem fs = cryptoFileSystem.get();
|
|
||||||
if (fs != null) {
|
|
||||||
return fs.getStats().pollBytesRead();
|
|
||||||
} else {
|
|
||||||
return 0l;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public long pollBytesWritten() {
|
|
||||||
CryptoFileSystem fs = cryptoFileSystem.get();
|
|
||||||
if (fs != null) {
|
|
||||||
return fs.getStats().pollBytesWritten();
|
|
||||||
} else {
|
|
||||||
return 0l;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCustomMountPath() {
|
|
||||||
return vaultSettings.individualMountPath().getValueSafe();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCustomMountPath(String mountPath) {
|
|
||||||
vaultSettings.individualMountPath().set(mountPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMountName() {
|
|
||||||
return vaultSettings.mountName().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMountName(String mountName) throws IllegalArgumentException {
|
|
||||||
if (StringUtils.isBlank(mountName)) {
|
|
||||||
throw new IllegalArgumentException("mount name is empty");
|
|
||||||
} else {
|
|
||||||
vaultSettings.mountName().set(VaultSettings.normalizeMountName(mountName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isHavingCustomMountFlags() {
|
public boolean isHavingCustomMountFlags() {
|
||||||
return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get());
|
return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get());
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMountFlags() {
|
public StringBinding defaultMountFlagsProperty() {
|
||||||
|
return defaultMountFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDefaultMountFlags() {
|
||||||
|
return defaultMountFlags.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEffectiveMountFlags() {
|
||||||
String mountFlags = vaultSettings.mountFlags().get();
|
String mountFlags = vaultSettings.mountFlags().get();
|
||||||
if (Strings.isNullOrEmpty(mountFlags)) {
|
if (Strings.isNullOrEmpty(mountFlags)) {
|
||||||
return defaultMountFlags.get();
|
return getDefaultMountFlags();
|
||||||
} else {
|
} else {
|
||||||
return mountFlags;
|
return mountFlags;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMountFlags(String mountFlags) {
|
public void setCustomMountFlags(String mountFlags) {
|
||||||
vaultSettings.mountFlags().set(mountFlags);
|
vaultSettings.mountFlags().set(mountFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Character getWinDriveLetter() {
|
|
||||||
if (vaultSettings.winDriveLetter().get() == null) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return vaultSettings.winDriveLetter().get().charAt(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setWinDriveLetter(Path root) {
|
|
||||||
if (root == null) {
|
|
||||||
vaultSettings.winDriveLetter().set(null);
|
|
||||||
} else {
|
|
||||||
char winDriveLetter = root.toString().charAt(0);
|
|
||||||
vaultSettings.winDriveLetter().set(String.valueOf(winDriveLetter));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return vaultSettings.getId();
|
return vaultSettings.getId();
|
||||||
}
|
}
|
||||||
+4
-1
@@ -3,7 +3,7 @@
|
|||||||
* All rights reserved. This program and the accompanying materials
|
* All rights reserved. This program and the accompanying materials
|
||||||
* are made available under the terms of the accompanying LICENSE file.
|
* are made available under the terms of the accompanying LICENSE file.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.ui.model;
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
import dagger.BindsInstance;
|
import dagger.BindsInstance;
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
import org.cryptomator.common.settings.VaultSettings;
|
||||||
@@ -22,6 +22,9 @@ public interface VaultComponent {
|
|||||||
@BindsInstance
|
@BindsInstance
|
||||||
Builder vaultSettings(VaultSettings vaultSettings);
|
Builder vaultSettings(VaultSettings vaultSettings);
|
||||||
|
|
||||||
|
@BindsInstance
|
||||||
|
Builder initialVaultState(VaultState vaultState);
|
||||||
|
|
||||||
VaultComponent build();
|
VaultComponent build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import org.cryptomator.common.settings.VaultSettings;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This listener makes sure to reflect any changes to the vault list back to the settings.
|
||||||
|
*/
|
||||||
|
class VaultListChangeListener implements ListChangeListener<Vault> {
|
||||||
|
|
||||||
|
private final ObservableList<VaultSettings> vaultSettingsList;
|
||||||
|
|
||||||
|
public VaultListChangeListener(ObservableList<VaultSettings> vaultSettingsList) {
|
||||||
|
this.vaultSettingsList = vaultSettingsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChanged(Change<? extends Vault> c) {
|
||||||
|
while(c.next()) {
|
||||||
|
if (c.wasAdded()) {
|
||||||
|
List<VaultSettings> addedSettings = c.getAddedSubList().stream().map(Vault::getVaultSettings).collect(Collectors.toList());
|
||||||
|
vaultSettingsList.addAll(c.getFrom(), addedSettings);
|
||||||
|
} else if (c.wasRemoved()) {
|
||||||
|
List<VaultSettings> removedSettings = c.getRemoved().stream().map(Vault::getVaultSettings).collect(Collectors.toList());
|
||||||
|
vaultSettingsList.removeAll(removedSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* 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.vaults;
|
||||||
|
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import org.cryptomator.common.settings.Settings;
|
||||||
|
import org.cryptomator.common.settings.VaultSettings;
|
||||||
|
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||||
|
import org.cryptomator.cryptofs.migration.Migrators;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class VaultListManager {
|
||||||
|
|
||||||
|
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
|
||||||
|
|
||||||
|
private final VaultComponent.Builder vaultComponentBuilder;
|
||||||
|
private final ObservableList<Vault> vaultList;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public VaultListManager(VaultComponent.Builder vaultComponentBuilder, Settings settings) {
|
||||||
|
this.vaultComponentBuilder = vaultComponentBuilder;
|
||||||
|
this.vaultList = FXCollections.observableArrayList(Vault::observables);
|
||||||
|
|
||||||
|
addAll(settings.getDirectories());
|
||||||
|
vaultList.addListener(new VaultListChangeListener(settings.getDirectories()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableList<Vault> getVaultList() {
|
||||||
|
return vaultList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vault add(Path pathToVault) throws NoSuchFileException {
|
||||||
|
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
|
||||||
|
throw new NoSuchFileException(pathToVault.toString(), null, "Not a vault directory");
|
||||||
|
}
|
||||||
|
Optional<Vault> alreadyExistingVault = get(pathToVault);
|
||||||
|
if (alreadyExistingVault.isPresent()) {
|
||||||
|
return alreadyExistingVault.get();
|
||||||
|
} else {
|
||||||
|
VaultSettings vaultSettings = VaultSettings.withRandomId();
|
||||||
|
vaultSettings.path().set(pathToVault);
|
||||||
|
Vault newVault = create(vaultSettings);
|
||||||
|
vaultList.add(newVault);
|
||||||
|
return newVault;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAll(Collection<VaultSettings> vaultSettings) {
|
||||||
|
Collection<Vault> vaults = vaultSettings.stream().map(this::create).collect(Collectors.toList());
|
||||||
|
vaultList.addAll(vaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Vault> get(Path vaultPath) {
|
||||||
|
return vaultList.stream().filter(v -> v.getPath().equals(vaultPath)).findAny();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vault create(VaultSettings vaultSettings) {
|
||||||
|
VaultState vaultState = determineVaultState(vaultSettings.path().get());
|
||||||
|
VaultComponent comp = vaultComponentBuilder.vaultSettings(vaultSettings).initialVaultState(vaultState).build();
|
||||||
|
return comp.vault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private VaultState determineVaultState(Path pathToVault) {
|
||||||
|
try {
|
||||||
|
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
|
||||||
|
return VaultState.MISSING;
|
||||||
|
} else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) {
|
||||||
|
return VaultState.NEEDS_MIGRATION;
|
||||||
|
} else {
|
||||||
|
return VaultState.LOCKED;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
return VaultState.ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+46
-27
@@ -3,14 +3,23 @@
|
|||||||
* All rights reserved. This program and the accompanying materials
|
* All rights reserved. This program and the accompanying materials
|
||||||
* are made available under the terms of the accompanying LICENSE file.
|
* are made available under the terms of the accompanying LICENSE file.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.ui.model;
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.StringBinding;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyStringProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
import org.cryptomator.common.settings.Settings;
|
import org.cryptomator.common.settings.Settings;
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
import org.cryptomator.common.settings.VaultSettings;
|
||||||
import org.cryptomator.common.settings.VolumeImpl;
|
import org.cryptomator.common.settings.VolumeImpl;
|
||||||
|
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -18,13 +27,25 @@ import java.io.IOException;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.function.Supplier;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
public class VaultModule {
|
public class VaultModule {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(VaultModule.class);
|
private static final Logger LOG = LoggerFactory.getLogger(VaultModule.class);
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@PerVault
|
||||||
|
public AtomicReference<CryptoFileSystem> provideCryptoFileSystemReference() {
|
||||||
|
return new AtomicReference<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@PerVault
|
||||||
|
public ObjectProperty<VaultState> provideVaultState(VaultState initialState) {
|
||||||
|
return new SimpleObjectProperty<>(initialState);
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
|
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
|
||||||
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
|
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
|
||||||
@@ -44,33 +65,33 @@ public class VaultModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@PerVault
|
@PerVault
|
||||||
@DefaultMountFlags
|
@DefaultMountFlags
|
||||||
public Supplier<String> provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
|
public StringBinding provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
|
||||||
return () -> {
|
ObjectProperty<VolumeImpl> preferredVolumeImpl = settings.preferredVolumeImpl();
|
||||||
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
|
StringProperty mountName = vaultSettings.mountName();
|
||||||
switch (preferredImpl) {
|
BooleanProperty readOnly = vaultSettings.usesReadOnlyMode();
|
||||||
case FUSE:
|
|
||||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
return Bindings.createStringBinding(() -> {
|
||||||
return getMacFuseDefaultMountFlags(settings, vaultSettings);
|
VolumeImpl v = preferredVolumeImpl.get();
|
||||||
} else if (SystemUtils.IS_OS_LINUX) {
|
if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_MAC) {
|
||||||
return getLinuxFuseDefaultMountFlags(settings, vaultSettings);
|
return getMacFuseDefaultMountFlags(mountName, readOnly);
|
||||||
}
|
} else if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_LINUX) {
|
||||||
case DOKANY:
|
return getLinuxFuseDefaultMountFlags(readOnly);
|
||||||
return getDokanyDefaultMountFlags(settings, vaultSettings);
|
} else if (v == VolumeImpl.DOKANY && SystemUtils.IS_OS_WINDOWS) {
|
||||||
default:
|
return getDokanyDefaultMountFlags(readOnly);
|
||||||
return "--flags-supported-on-FUSE-or-DOKANY-only";
|
} else {
|
||||||
|
return "--flags-supported-on-FUSE-or-DOKANY-only";
|
||||||
}
|
}
|
||||||
};
|
}, mountName, readOnly, preferredVolumeImpl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// see: https://github.com/osxfuse/osxfuse/wiki/Mount-options
|
// see: https://github.com/osxfuse/osxfuse/wiki/Mount-options
|
||||||
private String getMacFuseDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
|
private String getMacFuseDefaultMountFlags(ReadOnlyStringProperty mountName, ReadOnlyBooleanProperty readOnly) {
|
||||||
assert SystemUtils.IS_OS_MAC_OSX;
|
assert SystemUtils.IS_OS_MAC_OSX;
|
||||||
|
|
||||||
StringBuilder flags = new StringBuilder();
|
StringBuilder flags = new StringBuilder();
|
||||||
if (vaultSettings.usesReadOnlyMode().get()) {
|
if (readOnly.get()) {
|
||||||
flags.append(" -ordonly");
|
flags.append(" -ordonly");
|
||||||
}
|
}
|
||||||
flags.append(" -ovolname=").append(vaultSettings.mountName().get());
|
flags.append(" -ovolname=").append(mountName.get());
|
||||||
flags.append(" -oatomic_o_trunc");
|
flags.append(" -oatomic_o_trunc");
|
||||||
flags.append(" -oauto_xattr");
|
flags.append(" -oauto_xattr");
|
||||||
flags.append(" -oauto_cache");
|
flags.append(" -oauto_cache");
|
||||||
@@ -92,11 +113,10 @@ public class VaultModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// see https://manpages.debian.org/testing/fuse/mount.fuse.8.en.html
|
// see https://manpages.debian.org/testing/fuse/mount.fuse.8.en.html
|
||||||
private String getLinuxFuseDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
|
private String getLinuxFuseDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
|
||||||
assert SystemUtils.IS_OS_LINUX;
|
assert SystemUtils.IS_OS_LINUX;
|
||||||
|
|
||||||
StringBuilder flags = new StringBuilder();
|
StringBuilder flags = new StringBuilder();
|
||||||
if (vaultSettings.usesReadOnlyMode().get()) {
|
if (readOnly.get()) {
|
||||||
flags.append(" -oro");
|
flags.append(" -oro");
|
||||||
}
|
}
|
||||||
flags.append(" -oauto_unmount");
|
flags.append(" -oauto_unmount");
|
||||||
@@ -115,12 +135,11 @@ public class VaultModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// see https://github.com/cryptomator/dokany-nio-adapter/blob/develop/src/main/java/org/cryptomator/frontend/dokany/MountUtil.java#L30-L34
|
// see https://github.com/cryptomator/dokany-nio-adapter/blob/develop/src/main/java/org/cryptomator/frontend/dokany/MountUtil.java#L30-L34
|
||||||
private String getDokanyDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
|
private String getDokanyDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
|
||||||
assert SystemUtils.IS_OS_WINDOWS;
|
assert SystemUtils.IS_OS_WINDOWS;
|
||||||
|
|
||||||
StringBuilder flags = new StringBuilder();
|
StringBuilder flags = new StringBuilder();
|
||||||
flags.append(" --options CURRENT_SESSION");
|
flags.append(" --options CURRENT_SESSION");
|
||||||
if (vaultSettings.usesReadOnlyMode().get()) {
|
if (readOnly.get()) {
|
||||||
flags.append(",WRITE_PROTECTION");
|
flags.append(",WRITE_PROTECTION");
|
||||||
}
|
}
|
||||||
flags.append(" --thread-count 5");
|
flags.append(" --thread-count 5");
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
|
public enum VaultState {
|
||||||
|
/**
|
||||||
|
* No vault found at the provided path
|
||||||
|
*/
|
||||||
|
MISSING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vault requires migration to a newer vault format
|
||||||
|
*/
|
||||||
|
NEEDS_MIGRATION,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vault ready to be unlocked
|
||||||
|
*/
|
||||||
|
LOCKED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vault in transition between two other states
|
||||||
|
*/
|
||||||
|
PROCESSING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vault is unlocked
|
||||||
|
*/
|
||||||
|
UNLOCKED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unknown state due to preceeding unrecoverable exceptions.
|
||||||
|
*/
|
||||||
|
ERROR;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.Observable;
|
||||||
|
import javafx.beans.property.LongProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleLongProperty;
|
||||||
|
import javafx.concurrent.ScheduledService;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||||
|
import org.cryptomator.cryptofs.CryptoFileSystemStats;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
@PerVault
|
||||||
|
public class VaultStats {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(VaultStats.class);
|
||||||
|
|
||||||
|
private final AtomicReference<CryptoFileSystem> fs;
|
||||||
|
private final ObjectProperty<VaultState> state;
|
||||||
|
private final ScheduledService<Optional<CryptoFileSystemStats>> updateService;
|
||||||
|
private final LongProperty bytesPerSecondRead = new SimpleLongProperty();
|
||||||
|
private final LongProperty bytesPerSecondWritten = new SimpleLongProperty();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
VaultStats(AtomicReference<CryptoFileSystem> fs, ObjectProperty<VaultState> state, ExecutorService executor) {
|
||||||
|
this.fs = fs;
|
||||||
|
this.state = state;
|
||||||
|
this.updateService = new UpdateStatsService();
|
||||||
|
updateService.setExecutor(executor);
|
||||||
|
updateService.setPeriod(Duration.seconds(1));
|
||||||
|
|
||||||
|
state.addListener(this::vaultStateChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void vaultStateChanged(@SuppressWarnings("unused") Observable observable) {
|
||||||
|
switch (state.get()) {
|
||||||
|
case UNLOCKED:
|
||||||
|
assert fs.get() != null;
|
||||||
|
LOG.debug("start recording stats");
|
||||||
|
updateService.restart();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOG.debug("stop recording stats");
|
||||||
|
updateService.cancel();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStats(Optional<CryptoFileSystemStats> stats) {
|
||||||
|
assert Platform.isFxApplicationThread();
|
||||||
|
bytesPerSecondRead.set(stats.map(CryptoFileSystemStats::pollBytesRead).orElse(0l));
|
||||||
|
bytesPerSecondWritten.set(stats.map(CryptoFileSystemStats::pollBytesWritten).orElse(0l));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UpdateStatsService extends ScheduledService<Optional<CryptoFileSystemStats>> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<Optional<CryptoFileSystemStats>> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected Optional<CryptoFileSystemStats> call() {
|
||||||
|
return Optional.ofNullable(fs.get()).map(CryptoFileSystem::getStats);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void succeeded() {
|
||||||
|
assert getValue() != null;
|
||||||
|
updateStats(getValue());
|
||||||
|
super.succeeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Observables */
|
||||||
|
|
||||||
|
public LongProperty bytesPerSecondReadProperty() {
|
||||||
|
return bytesPerSecondRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getBytesPerSecondRead() {
|
||||||
|
return bytesPerSecondRead.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LongProperty bytesPerSecondWrittenProperty() {
|
||||||
|
return bytesPerSecondWritten;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getBytesPerSecondWritten() {
|
||||||
|
return bytesPerSecondWritten.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-1
@@ -1,9 +1,11 @@
|
|||||||
package org.cryptomator.ui.model;
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
import org.cryptomator.common.settings.VolumeImpl;
|
import org.cryptomator.common.settings.VolumeImpl;
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,6 +30,8 @@ public interface Volume {
|
|||||||
|
|
||||||
void unmount() throws VolumeException;
|
void unmount() throws VolumeException;
|
||||||
|
|
||||||
|
Optional<Path> getMountPoint();
|
||||||
|
|
||||||
// optional forced unmounting:
|
// optional forced unmounting:
|
||||||
|
|
||||||
default boolean supportsForcedUnmount() {
|
default boolean supportsForcedUnmount() {
|
||||||
+10
-2
@@ -1,4 +1,4 @@
|
|||||||
package org.cryptomator.ui.model;
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
|
|
||||||
import org.cryptomator.common.settings.Settings;
|
import org.cryptomator.common.settings.Settings;
|
||||||
@@ -13,6 +13,8 @@ import javax.inject.Inject;
|
|||||||
import javax.inject.Provider;
|
import javax.inject.Provider;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public class WebDavVolume implements Volume {
|
public class WebDavVolume implements Volume {
|
||||||
|
|
||||||
@@ -25,6 +27,7 @@ public class WebDavVolume implements Volume {
|
|||||||
private WebDavServer server;
|
private WebDavServer server;
|
||||||
private WebDavServletController servlet;
|
private WebDavServletController servlet;
|
||||||
private Mounter.Mount mount;
|
private Mounter.Mount mount;
|
||||||
|
private Path mountPoint;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings) {
|
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings) {
|
||||||
@@ -52,7 +55,7 @@ public class WebDavVolume implements Volume {
|
|||||||
}
|
}
|
||||||
MountParams mountParams = MountParams.create() //
|
MountParams mountParams = MountParams.create() //
|
||||||
.withWindowsDriveLetter(vaultSettings.winDriveLetter().get()) //
|
.withWindowsDriveLetter(vaultSettings.winDriveLetter().get()) //
|
||||||
.withPreferredGvfsScheme(settings.preferredGvfsScheme().get())//
|
.withPreferredGvfsScheme(settings.preferredGvfsScheme().get().getPrefix())//
|
||||||
.withWebdavHostname(getLocalhostAliasOrNull()) //
|
.withWebdavHostname(getLocalhostAliasOrNull()) //
|
||||||
.build();
|
.build();
|
||||||
try {
|
try {
|
||||||
@@ -93,6 +96,11 @@ public class WebDavVolume implements Volume {
|
|||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Path> getMountPoint() {
|
||||||
|
return Optional.ofNullable(mountPoint);
|
||||||
|
}
|
||||||
|
|
||||||
private String getLocalhostAliasOrNull() {
|
private String getLocalhostAliasOrNull() {
|
||||||
try {
|
try {
|
||||||
InetAddress alias = InetAddress.getByName(LOCALHOST_ALIAS);
|
InetAddress alias = InetAddress.getByName(LOCALHOST_ALIAS);
|
||||||
+3
-3
@@ -3,14 +3,14 @@
|
|||||||
* All rights reserved. This program and the accompanying materials
|
* All rights reserved. This program and the accompanying materials
|
||||||
* are made available under the terms of the accompanying LICENSE file.
|
* are made available under the terms of the accompanying LICENSE file.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.ui.model;
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
import org.cryptomator.common.FxApplicationScoped;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
import java.nio.file.FileSystems;
|
import java.nio.file.FileSystems;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -19,7 +19,7 @@ import java.util.stream.Collectors;
|
|||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
import java.util.stream.StreamSupport;
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
@FxApplicationScoped
|
@Singleton
|
||||||
public final class WindowsDriveLetters {
|
public final class WindowsDriveLetters {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(WindowsDriveLetters.class);
|
private static final Logger LOG = LoggerFactory.getLogger(WindowsDriveLetters.class);
|
||||||
+1
-1
@@ -30,7 +30,7 @@ public class SettingsJsonAdapterTest {
|
|||||||
Assertions.assertEquals(2, settings.getDirectories().size());
|
Assertions.assertEquals(2, settings.getDirectories().size());
|
||||||
Assertions.assertEquals(8080, settings.port().get());
|
Assertions.assertEquals(8080, settings.port().get());
|
||||||
Assertions.assertEquals(42, settings.numTrayNotifications().get());
|
Assertions.assertEquals(42, settings.numTrayNotifications().get());
|
||||||
Assertions.assertEquals("dav", settings.preferredGvfsScheme().get());
|
Assertions.assertEquals(WebDavUrlScheme.DAV, settings.preferredGvfsScheme().get());
|
||||||
Assertions.assertEquals(VolumeImpl.FUSE, settings.preferredVolumeImpl().get());
|
Assertions.assertEquals(VolumeImpl.FUSE, settings.preferredVolumeImpl().get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,22 +8,20 @@ package org.cryptomator.common.settings;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class SettingsTest {
|
public class SettingsTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAutoSave() throws IOException {
|
public void testAutoSave() {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked") Consumer<Settings> changeListener = Mockito.mock(Consumer.class);
|
||||||
Consumer<Settings> changeListener = Mockito.mock(Consumer.class);
|
|
||||||
Settings settings = new Settings();
|
Settings settings = new Settings();
|
||||||
settings.setSaveCmd(changeListener);
|
settings.setSaveCmd(changeListener);
|
||||||
VaultSettings vaultSettings = VaultSettings.withRandomId();
|
VaultSettings vaultSettings = VaultSettings.withRandomId();
|
||||||
Mockito.verify(changeListener, Mockito.times(0)).accept(settings);
|
Mockito.verify(changeListener, Mockito.times(0)).accept(settings);
|
||||||
|
|
||||||
// first change (to property):
|
// first change (to property):
|
||||||
settings.preferredGvfsScheme().set("asd");
|
settings.preferredGvfsScheme().set(WebDavUrlScheme.WEBDAV);
|
||||||
Mockito.verify(changeListener, Mockito.times(1)).accept(settings);
|
Mockito.verify(changeListener, Mockito.times(1)).accept(settings);
|
||||||
|
|
||||||
// second change (to list):
|
// second change (to list):
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package org.cryptomator.common.vaults;
|
||||||
|
|
||||||
|
import javafx.beans.binding.StringBinding;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import org.cryptomator.common.settings.Settings;
|
||||||
|
import org.cryptomator.common.settings.VaultSettings;
|
||||||
|
import org.cryptomator.common.settings.VolumeImpl;
|
||||||
|
import org.hamcrest.CoreMatchers;
|
||||||
|
import org.hamcrest.MatcherAssert;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.condition.EnabledOnOs;
|
||||||
|
import org.junit.jupiter.api.condition.OS;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class VaultModuleTest {
|
||||||
|
|
||||||
|
private final Settings settings = Mockito.mock(Settings.class);
|
||||||
|
private final VaultSettings vaultSettings = Mockito.mock(VaultSettings.class);
|
||||||
|
|
||||||
|
private final VaultModule module = new VaultModule();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup(@TempDir Path tmpDir) {
|
||||||
|
Mockito.when(vaultSettings.mountName()).thenReturn(new SimpleStringProperty("TEST"));
|
||||||
|
Mockito.when(vaultSettings.usesReadOnlyMode()).thenReturn(new SimpleBooleanProperty(true));
|
||||||
|
System.setProperty("user.home", tmpDir.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("provideDefaultMountFlags on Mac/FUSE")
|
||||||
|
@EnabledOnOs(OS.MAC)
|
||||||
|
public void testMacFuseDefaultMountFlags() {
|
||||||
|
Mockito.when(settings.preferredVolumeImpl()).thenReturn(new SimpleObjectProperty<>(VolumeImpl.FUSE));
|
||||||
|
|
||||||
|
StringBinding result = module.provideDefaultMountFlags(settings, vaultSettings);
|
||||||
|
|
||||||
|
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ovolname=TEST"));
|
||||||
|
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ordonly"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("provideDefaultMountFlags on Linux/FUSE")
|
||||||
|
@EnabledOnOs(OS.LINUX)
|
||||||
|
public void testLinuxFuseDefaultMountFlags() {
|
||||||
|
Mockito.when(settings.preferredVolumeImpl()).thenReturn(new SimpleObjectProperty<>(VolumeImpl.FUSE));
|
||||||
|
|
||||||
|
StringBinding result = module.provideDefaultMountFlags(settings, vaultSettings);
|
||||||
|
|
||||||
|
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-oro"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("provideDefaultMountFlags on Windows/Dokany")
|
||||||
|
@EnabledOnOs(OS.WINDOWS)
|
||||||
|
public void testWinDokanyDefaultMountFlags() {
|
||||||
|
Mockito.when(settings.preferredVolumeImpl()).thenReturn(new SimpleObjectProperty<>(VolumeImpl.DOKANY));
|
||||||
|
|
||||||
|
StringBinding result = module.provideDefaultMountFlags(settings, vaultSettings);
|
||||||
|
|
||||||
|
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("--options CURRENT_SESSION,WRITE_PROTECTION"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.4.16</version>
|
<version>1.5.0-alpha1</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>keychain</artifactId>
|
<artifactId>keychain</artifactId>
|
||||||
<name>System Keychain Access</name>
|
<name>System Keychain Access</name>
|
||||||
@@ -23,11 +23,7 @@
|
|||||||
<groupId>com.google.code.gson</groupId>
|
<groupId>com.google.code.gson</groupId>
|
||||||
<artifactId>gson</artifactId>
|
<artifactId>gson</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.cryptomator</groupId>
|
|
||||||
<artifactId>jni</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Google -->
|
<!-- Google -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.guava</groupId>
|
<groupId>com.google.guava</groupId>
|
||||||
|
|||||||
@@ -5,30 +5,18 @@
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.keychain;
|
package org.cryptomator.keychain;
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
import dagger.multibindings.ElementsIntoSet;
|
import dagger.multibindings.ElementsIntoSet;
|
||||||
import org.cryptomator.jni.JniFunctions;
|
import org.cryptomator.common.JniModule;
|
||||||
import org.cryptomator.jni.MacFunctions;
|
|
||||||
import org.cryptomator.jni.WinFunctions;
|
|
||||||
|
|
||||||
@Module
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Module(includes = {JniModule.class})
|
||||||
public class KeychainModule {
|
public class KeychainModule {
|
||||||
|
|
||||||
@Provides
|
|
||||||
Optional<MacFunctions> provideOptionalMacFunctions() {
|
|
||||||
return JniFunctions.macFunctions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
Optional<WinFunctions> provideOptionalWinFunctions() {
|
|
||||||
return JniFunctions.winFunctions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ElementsIntoSet
|
@ElementsIntoSet
|
||||||
Set<KeychainAccessStrategy> provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain, LinuxSecretServiceKeychainAccess linKeychain) {
|
Set<KeychainAccessStrategy> provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain, LinuxSecretServiceKeychainAccess linKeychain) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.4.16</version>
|
<version>1.5.0-alpha1</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>launcher</artifactId>
|
<artifactId>launcher</artifactId>
|
||||||
<name>Cryptomator Launcher</name>
|
<name>Cryptomator Launcher</name>
|
||||||
|
|||||||
@@ -11,30 +11,38 @@ import java.util.concurrent.ConcurrentMap;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
class CleanShutdownPerformer extends Thread {
|
class CleanShutdownPerformer extends Thread {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(CleanShutdownPerformer.class);
|
private static final Logger LOG = LoggerFactory.getLogger(CleanShutdownPerformer.class);
|
||||||
static final ConcurrentMap<Runnable, Boolean> SHUTDOWN_TASKS = new ConcurrentHashMap<>();
|
private final ConcurrentMap<Runnable, Boolean> tasks = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CleanShutdownPerformer() {
|
||||||
|
super(null, null, "ShutdownTasks", 0);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
LOG.debug("Running graceful shutdown tasks...");
|
LOG.debug("Running graceful shutdown tasks...");
|
||||||
SHUTDOWN_TASKS.keySet().forEach(r -> {
|
tasks.keySet().forEach(r -> {
|
||||||
try {
|
try {
|
||||||
r.run();
|
r.run();
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
LOG.error("Exception while shutting down.", e);
|
LOG.error("Exception while shutting down.", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
SHUTDOWN_TASKS.clear();
|
tasks.clear();
|
||||||
LOG.info("Goodbye.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void scheduleShutdownTask(Runnable task) {
|
void scheduleShutdownTask(Runnable task) {
|
||||||
SHUTDOWN_TASKS.put(task, Boolean.TRUE);
|
tasks.put(task, Boolean.TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void registerShutdownHook() {
|
void registerShutdownHook() {
|
||||||
Runtime.getRuntime().addShutdownHook(new CleanShutdownPerformer());
|
Runtime.getRuntime().addShutdownHook(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,12 +5,10 @@
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.launcher;
|
package org.cryptomator.launcher;
|
||||||
|
|
||||||
import javafx.application.Application;
|
|
||||||
import javafx.stage.Stage;
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
import org.cryptomator.logging.DebugMode;
|
import org.cryptomator.logging.DebugMode;
|
||||||
import org.cryptomator.logging.LoggerConfiguration;
|
import org.cryptomator.logging.LoggerConfiguration;
|
||||||
import org.cryptomator.ui.controllers.MainController;
|
import org.cryptomator.ui.launcher.UiLauncher;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -19,6 +17,7 @@ import javax.inject.Named;
|
|||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class Cryptomator {
|
public class Cryptomator {
|
||||||
@@ -32,13 +31,19 @@ public class Cryptomator {
|
|||||||
private final DebugMode debugMode;
|
private final DebugMode debugMode;
|
||||||
private final IpcFactory ipcFactory;
|
private final IpcFactory ipcFactory;
|
||||||
private final Optional<String> applicationVersion;
|
private final Optional<String> applicationVersion;
|
||||||
|
private final CountDownLatch shutdownLatch;
|
||||||
|
private final CleanShutdownPerformer shutdownPerformer;
|
||||||
|
private final UiLauncher uiLauncher;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> applicationVersion) {
|
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, CleanShutdownPerformer shutdownPerformer, UiLauncher uiLauncher) {
|
||||||
this.logConfig = logConfig;
|
this.logConfig = logConfig;
|
||||||
this.debugMode = debugMode;
|
this.debugMode = debugMode;
|
||||||
this.ipcFactory = ipcFactory;
|
this.ipcFactory = ipcFactory;
|
||||||
this.applicationVersion = applicationVersion;
|
this.applicationVersion = applicationVersion;
|
||||||
|
this.shutdownLatch = shutdownLatch;
|
||||||
|
this.shutdownPerformer = shutdownPerformer;
|
||||||
|
this.uiLauncher = uiLauncher;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
@@ -48,6 +53,7 @@ public class Cryptomator {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Main entry point of the application launcher.
|
* Main entry point of the application launcher.
|
||||||
|
*
|
||||||
* @param args The arguments passed to this program via {@link #main(String[])}.
|
* @param args The arguments passed to this program via {@link #main(String[])}.
|
||||||
* @return Nonzero exit code in case of an error.
|
* @return Nonzero exit code in case of an error.
|
||||||
*/
|
*/
|
||||||
@@ -63,6 +69,7 @@ public class Cryptomator {
|
|||||||
try (IpcFactory.IpcEndpoint endpoint = ipcFactory.create()) {
|
try (IpcFactory.IpcEndpoint endpoint = ipcFactory.create()) {
|
||||||
endpoint.getRemote().handleLaunchArgs(args); // if we are the server, getRemote() returns self.
|
endpoint.getRemote().handleLaunchArgs(args); // if we are the server, getRemote() returns self.
|
||||||
if (endpoint.isConnectedToRemote()) {
|
if (endpoint.isConnectedToRemote()) {
|
||||||
|
endpoint.getRemote().revealRunningApp();
|
||||||
LOG.info("Found running application instance. Shutting down...");
|
LOG.info("Found running application instance. Shutting down...");
|
||||||
return 2;
|
return 2;
|
||||||
} else {
|
} else {
|
||||||
@@ -77,45 +84,22 @@ public class Cryptomator {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Launches the JavaFX application and waits until shutdown is requested.
|
* Launches the JavaFX application and waits until shutdown is requested.
|
||||||
|
*
|
||||||
* @return Nonzero exit code in case of an error.
|
* @return Nonzero exit code in case of an error.
|
||||||
|
* @implNote This method blocks until {@link #shutdownLatch} reached zero.
|
||||||
*/
|
*/
|
||||||
private int runGuiApplication() {
|
private int runGuiApplication() {
|
||||||
try {
|
try {
|
||||||
CleanShutdownPerformer.registerShutdownHook();
|
shutdownPerformer.registerShutdownHook();
|
||||||
Application.launch(MainApp.class);
|
uiLauncher.launch();
|
||||||
LOG.info("Shutting down...");
|
shutdownLatch.await();
|
||||||
|
LOG.info("UI shut down");
|
||||||
return 0;
|
return 0;
|
||||||
} catch (Throwable e) {
|
} catch (InterruptedException e) {
|
||||||
LOG.error("Terminating due to error", e);
|
Thread.currentThread().interrupt();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// We need a separate FX Application class, until we can use the module system. See https://stackoverflow.com/q/54756176/4014509
|
|
||||||
public static class MainApp extends Application {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start(Stage primaryStage) {
|
|
||||||
LOG.info("JavaFX application started.");
|
|
||||||
primaryStage.setMinWidth(652.0);
|
|
||||||
primaryStage.setMinHeight(440.0);
|
|
||||||
|
|
||||||
FxApplicationComponent fxApplicationComponent = CRYPTOMATOR_COMPONENT.fxApplicationComponent() //
|
|
||||||
.fxApplication(this) //
|
|
||||||
.mainWindow(primaryStage) //
|
|
||||||
.build();
|
|
||||||
|
|
||||||
MainController mainCtrl = fxApplicationComponent.fxmlLoader().load("/fxml/main.fxml");
|
|
||||||
mainCtrl.initStage(primaryStage);
|
|
||||||
primaryStage.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
LOG.info("JavaFX application stopped.");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ package org.cryptomator.launcher;
|
|||||||
import dagger.Component;
|
import dagger.Component;
|
||||||
import org.cryptomator.common.CommonsModule;
|
import org.cryptomator.common.CommonsModule;
|
||||||
import org.cryptomator.logging.LoggerModule;
|
import org.cryptomator.logging.LoggerModule;
|
||||||
|
import org.cryptomator.ui.launcher.UiLauncherModule;
|
||||||
|
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class})
|
@Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class, UiLauncherModule.class})
|
||||||
public interface CryptomatorComponent {
|
public interface CryptomatorComponent {
|
||||||
|
|
||||||
Cryptomator application();
|
Cryptomator application();
|
||||||
|
|
||||||
FxApplicationComponent.Builder fxApplicationComponent();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,31 +2,28 @@ package org.cryptomator.launcher;
|
|||||||
|
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
import org.cryptomator.common.settings.Settings;
|
|
||||||
import org.cryptomator.common.settings.SettingsProvider;
|
|
||||||
import org.cryptomator.ui.model.AppLaunchEvent;
|
|
||||||
|
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
|
||||||
import java.util.concurrent.BlockingQueue;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
class CryptomatorModule {
|
class CryptomatorModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
static Settings provideSettings(SettingsProvider settingsProvider) {
|
@Named("shutdownTaskScheduler")
|
||||||
return settingsProvider.get();
|
Consumer<Runnable> provideShutdownTaskScheduler(CleanShutdownPerformer shutdownPerformer) {
|
||||||
|
return shutdownPerformer::scheduleShutdownTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@Named("launchEventQueue")
|
@Named("shutdownLatch")
|
||||||
static BlockingQueue<AppLaunchEvent> provideFileOpenRequests() {
|
static CountDownLatch provideShutdownLatch() {
|
||||||
return new ArrayBlockingQueue<>(10);
|
return new CountDownLatch(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.launcher;
|
package org.cryptomator.launcher;
|
||||||
|
|
||||||
import org.cryptomator.ui.model.AppLaunchEvent;
|
import org.cryptomator.ui.launcher.AppLaunchEvent;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -34,16 +34,14 @@ class FileOpenRequestHandler {
|
|||||||
@Inject
|
@Inject
|
||||||
public FileOpenRequestHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
|
public FileOpenRequestHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
|
||||||
this.launchEventQueue = launchEventQueue;
|
this.launchEventQueue = launchEventQueue;
|
||||||
try {
|
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.APP_OPEN_FILE)) {
|
||||||
Desktop.getDesktop().setOpenFileHandler(this::openFiles);
|
Desktop.getDesktop().setOpenFileHandler(this::openFiles);
|
||||||
} catch (UnsupportedOperationException e) {
|
|
||||||
LOG.info("Unable to setOpenFileHandler, probably not supported on this OS.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openFiles(final OpenFilesEvent evt) {
|
private void openFiles(OpenFilesEvent evt) {
|
||||||
Stream<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath);
|
Stream<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath);
|
||||||
AppLaunchEvent launchEvent = new AppLaunchEvent(pathsToOpen);
|
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
|
||||||
tryToEnqueueFileOpenRequest(launchEvent);
|
tryToEnqueueFileOpenRequest(launchEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +59,7 @@ class FileOpenRequestHandler {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}).filter(Objects::nonNull);
|
}).filter(Objects::nonNull);
|
||||||
AppLaunchEvent launchEvent = new AppLaunchEvent(pathsToOpen);
|
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
|
||||||
tryToEnqueueFileOpenRequest(launchEvent);
|
tryToEnqueueFileOpenRequest(launchEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the accompanying LICENSE file.
|
|
||||||
*******************************************************************************/
|
|
||||||
package org.cryptomator.launcher;
|
|
||||||
|
|
||||||
import dagger.Module;
|
|
||||||
import dagger.Provides;
|
|
||||||
import org.cryptomator.common.FxApplicationScoped;
|
|
||||||
import org.cryptomator.ui.UiModule;
|
|
||||||
|
|
||||||
import javax.inject.Named;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
@Module(includes = {UiModule.class})
|
|
||||||
class FxApplicationModule {
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@FxApplicationScoped
|
|
||||||
@Named("shutdownTaskScheduler")
|
|
||||||
Consumer<Runnable> provideShutdownTaskScheduler() {
|
|
||||||
return CleanShutdownPerformer::scheduleShutdownTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,8 @@ import java.rmi.RemoteException;
|
|||||||
|
|
||||||
interface IpcProtocol extends Remote {
|
interface IpcProtocol extends Remote {
|
||||||
|
|
||||||
void handleLaunchArgs(String[] args) throws RemoteException;
|
void revealRunningApp() throws RemoteException;
|
||||||
|
|
||||||
|
void handleLaunchArgs(String... args) throws RemoteException;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
package org.cryptomator.launcher;
|
package org.cryptomator.launcher;
|
||||||
|
|
||||||
|
import org.cryptomator.ui.launcher.AppLaunchEvent;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class IpcProtocolImpl implements IpcProtocol {
|
class IpcProtocolImpl implements IpcProtocol {
|
||||||
@@ -13,15 +17,22 @@ class IpcProtocolImpl implements IpcProtocol {
|
|||||||
private static final Logger LOG = LoggerFactory.getLogger(IpcProtocolImpl.class);
|
private static final Logger LOG = LoggerFactory.getLogger(IpcProtocolImpl.class);
|
||||||
|
|
||||||
private final FileOpenRequestHandler fileOpenRequestHandler;
|
private final FileOpenRequestHandler fileOpenRequestHandler;
|
||||||
|
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public IpcProtocolImpl(FileOpenRequestHandler fileOpenRequestHandler) {
|
public IpcProtocolImpl(FileOpenRequestHandler fileOpenRequestHandler, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
|
||||||
this.fileOpenRequestHandler = fileOpenRequestHandler;
|
this.fileOpenRequestHandler = fileOpenRequestHandler;
|
||||||
|
this.launchEventQueue = launchEventQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleLaunchArgs(String[] args) {
|
public void revealRunningApp() {
|
||||||
LOG.info("Received launch args: {}", Arrays.stream(args).reduce((a, b) -> a + ", " + b).orElse(""));
|
launchEventQueue.add(new AppLaunchEvent(AppLaunchEvent.EventType.REVEAL_APP, Stream.empty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleLaunchArgs(String... args) {
|
||||||
|
LOG.debug("Received launch args: {}", Arrays.stream(args).reduce((a, b) -> a + ", " + b).orElse(""));
|
||||||
fileOpenRequestHandler.handleLaunchArgs(args);
|
fileOpenRequestHandler.handleLaunchArgs(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.launcher;
|
package org.cryptomator.launcher;
|
||||||
|
|
||||||
import org.cryptomator.ui.model.AppLaunchEvent;
|
import org.cryptomator.ui.launcher.AppLaunchEvent;
|
||||||
import org.hamcrest.CoreMatchers;
|
import org.hamcrest.CoreMatchers;
|
||||||
import org.hamcrest.MatcherAssert;
|
import org.hamcrest.MatcherAssert;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
@@ -64,7 +64,7 @@ public class FileOpenRequestHandlerTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("./cryptomator.exe foo (with full event queue)")
|
@DisplayName("./cryptomator.exe foo (with full event queue)")
|
||||||
public void testOpenArgsWithFullQueue() throws IOException {
|
public void testOpenArgsWithFullQueue() throws IOException {
|
||||||
queue.add(new AppLaunchEvent(Stream.empty()));
|
queue.add(new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, Stream.empty()));
|
||||||
Assumptions.assumeTrue(queue.remainingCapacity() == 0);
|
Assumptions.assumeTrue(queue.remainingCapacity() == 0);
|
||||||
|
|
||||||
inTest.handleLaunchArgs(new String[]{"foo"});
|
inTest.handleLaunchArgs(new String[]{"foo"});
|
||||||
|
|||||||
+9
-14
@@ -3,7 +3,7 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.4.16</version>
|
<version>1.5.0-alpha1</version>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<name>Cryptomator</name>
|
<name>Cryptomator</name>
|
||||||
|
|
||||||
@@ -24,9 +24,9 @@
|
|||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|
||||||
<!-- dependency versions -->
|
<!-- dependency versions -->
|
||||||
<cryptomator.cryptolib.version>1.2.1</cryptomator.cryptolib.version>
|
<cryptomator.cryptolib.version>1.2.2</cryptomator.cryptolib.version>
|
||||||
<cryptomator.cryptofs.version>1.8.8</cryptomator.cryptofs.version>
|
<cryptomator.cryptofs.version>1.9.0-beta1</cryptomator.cryptofs.version>
|
||||||
<cryptomator.jni.version>2.0.0</cryptomator.jni.version>
|
<cryptomator.jni.version>2.2.1</cryptomator.jni.version>
|
||||||
<cryptomator.fuse.version>1.2.0</cryptomator.fuse.version>
|
<cryptomator.fuse.version>1.2.0</cryptomator.fuse.version>
|
||||||
<cryptomator.dokany.version>1.1.11</cryptomator.dokany.version>
|
<cryptomator.dokany.version>1.1.11</cryptomator.dokany.version>
|
||||||
<cryptomator.webdav.version>1.0.10</cryptomator.webdav.version>
|
<cryptomator.webdav.version>1.0.10</cryptomator.webdav.version>
|
||||||
@@ -51,16 +51,6 @@
|
|||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<repositories>
|
<repositories>
|
||||||
<repository>
|
|
||||||
<id>ossrh-snapshots</id>
|
|
||||||
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
|
|
||||||
<releases>
|
|
||||||
<enabled>false</enabled>
|
|
||||||
</releases>
|
|
||||||
<snapshots>
|
|
||||||
<enabled>true</enabled>
|
|
||||||
</snapshots>
|
|
||||||
</repository>
|
|
||||||
<repository>
|
<repository>
|
||||||
<id>jcenter</id>
|
<id>jcenter</id>
|
||||||
<url>http://jcenter.bintray.com</url>
|
<url>http://jcenter.bintray.com</url>
|
||||||
@@ -129,6 +119,11 @@
|
|||||||
<artifactId>javafx-base</artifactId>
|
<artifactId>javafx-base</artifactId>
|
||||||
<version>${javafx.version}</version>
|
<version>${javafx.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-graphics</artifactId>
|
||||||
|
<version>${javafx.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-controls</artifactId>
|
<artifactId>javafx-controls</artifactId>
|
||||||
|
|||||||
+3
-22
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.4.16</version>
|
<version>1.5.0-alpha1</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>ui</artifactId>
|
<artifactId>ui</artifactId>
|
||||||
<name>Cryptomator GUI</name>
|
<name>Cryptomator GUI</name>
|
||||||
@@ -18,29 +18,10 @@
|
|||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>commons</artifactId>
|
<artifactId>commons</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.cryptomator</groupId>
|
|
||||||
<artifactId>cryptofs</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>jni</artifactId>
|
<artifactId>jni</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.cryptomator</groupId>
|
|
||||||
<artifactId>fuse-nio-adapter</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.cryptomator</groupId>
|
|
||||||
<artifactId>dokany-nio-adapter</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.cryptomator</groupId>
|
|
||||||
<artifactId>webdav-nio-adapter</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- CryptoLib -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>cryptolib</artifactId>
|
<artifactId>cryptolib</artifactId>
|
||||||
@@ -72,7 +53,7 @@
|
|||||||
<artifactId>gson</artifactId>
|
<artifactId>gson</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- apache commons -->
|
<!-- Apache Commons -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>commons-io</groupId>
|
<groupId>commons-io</groupId>
|
||||||
<artifactId>commons-io</artifactId>
|
<artifactId>commons-io</artifactId>
|
||||||
@@ -92,7 +73,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.nulab-inc</groupId>
|
<groupId>com.nulab-inc</groupId>
|
||||||
<artifactId>zxcvbn</artifactId>
|
<artifactId>zxcvbn</artifactId>
|
||||||
<version>1.2.2</version>
|
<version>1.2.7</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Logging -->
|
<!-- Logging -->
|
||||||
|
|||||||
@@ -1,216 +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
|
|
||||||
* Jean-Noël Charon - implementation of github issue #56
|
|
||||||
*******************************************************************************/
|
|
||||||
package org.cryptomator.ui;
|
|
||||||
|
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.stage.Stage;
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.cryptomator.common.FxApplicationScoped;
|
|
||||||
import org.cryptomator.common.settings.Settings;
|
|
||||||
import org.cryptomator.jni.JniException;
|
|
||||||
import org.cryptomator.jni.MacApplicationUiState;
|
|
||||||
import org.cryptomator.jni.MacFunctions;
|
|
||||||
import org.cryptomator.ui.l10n.Localization;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import javax.script.ScriptEngine;
|
|
||||||
import javax.script.ScriptEngineManager;
|
|
||||||
import javax.script.ScriptException;
|
|
||||||
import javax.swing.SwingUtilities;
|
|
||||||
import java.awt.AWTException;
|
|
||||||
import java.awt.Image;
|
|
||||||
import java.awt.MenuItem;
|
|
||||||
import java.awt.PopupMenu;
|
|
||||||
import java.awt.SystemTray;
|
|
||||||
import java.awt.Toolkit;
|
|
||||||
import java.awt.TrayIcon;
|
|
||||||
import java.awt.TrayIcon.MessageType;
|
|
||||||
import java.awt.event.ActionEvent;
|
|
||||||
import java.awt.event.MouseAdapter;
|
|
||||||
import java.awt.event.MouseEvent;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
@FxApplicationScoped
|
|
||||||
public class ExitUtil {
|
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ExitUtil.class);
|
|
||||||
|
|
||||||
private final Stage mainWindow;
|
|
||||||
private final Localization localization;
|
|
||||||
private final Settings settings;
|
|
||||||
private final Optional<MacFunctions> macFunctions;
|
|
||||||
private TrayIcon trayIcon;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public ExitUtil(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, Optional<MacFunctions> macFunctions) {
|
|
||||||
this.mainWindow = mainWindow;
|
|
||||||
this.localization = localization;
|
|
||||||
this.settings = settings;
|
|
||||||
this.macFunctions = macFunctions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initExitHandler(Runnable exitCommand) {
|
|
||||||
if (SystemUtils.IS_OS_LINUX) {
|
|
||||||
initMinimizeExitHandler(exitCommand);
|
|
||||||
} else {
|
|
||||||
initTrayIconExitHandler(exitCommand);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initMinimizeExitHandler(Runnable exitCommand) {
|
|
||||||
mainWindow.setOnCloseRequest(e -> {
|
|
||||||
if (Platform.isImplicitExit()) {
|
|
||||||
exitCommand.run();
|
|
||||||
} else {
|
|
||||||
mainWindow.setIconified(true);
|
|
||||||
e.consume();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initTrayIconExitHandler(Runnable exitCommand) {
|
|
||||||
trayIcon = createTrayIcon(exitCommand);
|
|
||||||
try {
|
|
||||||
// double clicking tray icon should open Cryptomator
|
|
||||||
if (SystemUtils.IS_OS_WINDOWS) {
|
|
||||||
trayIcon.addMouseListener(new TrayIconMouseListener());
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemTray.getSystemTray().add(trayIcon);
|
|
||||||
mainWindow.setOnCloseRequest((e) -> {
|
|
||||||
if (Platform.isImplicitExit()) {
|
|
||||||
exitCommand.run();
|
|
||||||
} else {
|
|
||||||
macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToAgentApplication));
|
|
||||||
mainWindow.close();
|
|
||||||
this.showTrayNotification(trayIcon);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (SecurityException | AWTException ex) {
|
|
||||||
// not working? then just go ahead and close the app
|
|
||||||
mainWindow.setOnCloseRequest((ev) -> {
|
|
||||||
exitCommand.run();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private TrayIcon createTrayIcon(Runnable exitCommand) {
|
|
||||||
final PopupMenu popup = new PopupMenu();
|
|
||||||
|
|
||||||
final MenuItem showItem = new MenuItem(localization.getString("tray.menu.open"));
|
|
||||||
showItem.addActionListener(this::restoreFromTray);
|
|
||||||
popup.add(showItem);
|
|
||||||
|
|
||||||
final MenuItem exitItem = new MenuItem(localization.getString("tray.menu.quit"));
|
|
||||||
exitItem.addActionListener(e -> exitCommand.run());
|
|
||||||
popup.add(exitItem);
|
|
||||||
|
|
||||||
final Image image = getAppropriateTrayIconImage(true);
|
|
||||||
|
|
||||||
return new TrayIcon(image, localization.getString("app.name"), popup);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return true if <code>defaults read -g AppleInterfaceStyle</code> has an exit status of <code>0</code> (i.e. _not_ returning "key not found").
|
|
||||||
*/
|
|
||||||
private boolean isMacMenuBarDarkMode() {
|
|
||||||
try {
|
|
||||||
// check for exit status only. Once there are more modes than "dark" and "default", we might need to analyze string contents..
|
|
||||||
final Process proc = Runtime.getRuntime().exec(new String[] {"defaults", "read", "-g", "AppleInterfaceStyle"});
|
|
||||||
proc.waitFor(100, TimeUnit.MILLISECONDS);
|
|
||||||
return proc.exitValue() == 0;
|
|
||||||
} catch (IOException | InterruptedException | IllegalThreadStateException ex) {
|
|
||||||
// IllegalThreadStateException thrown by proc.exitValue(), if process didn't terminate
|
|
||||||
LOG.warn("Determining MAC OS X dark mode settings failed. Assuming default (light) mode.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showTrayNotification(TrayIcon trayIcon) {
|
|
||||||
int remainingTrayNotification = settings.numTrayNotifications().get();
|
|
||||||
if (remainingTrayNotification <= 0) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
settings.numTrayNotifications().set(remainingTrayNotification - 1);
|
|
||||||
}
|
|
||||||
final Runnable notificationCmd;
|
|
||||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
|
||||||
final String title = localization.getString("tray.infoMsg.title");
|
|
||||||
final String msg = localization.getString("tray.infoMsg.msg.osx");
|
|
||||||
final String notificationCenterAppleScript = String.format("display notification \"%s\" with title \"%s\"", msg, title);
|
|
||||||
notificationCmd = () -> {
|
|
||||||
try {
|
|
||||||
final ScriptEngineManager mgr = new ScriptEngineManager();
|
|
||||||
final ScriptEngine engine = mgr.getEngineByName("AppleScriptEngine");
|
|
||||||
if (engine != null) {
|
|
||||||
engine.eval(notificationCenterAppleScript);
|
|
||||||
} else {
|
|
||||||
Runtime.getRuntime().exec(new String[] {"/usr/bin/osascript", "-e", notificationCenterAppleScript});
|
|
||||||
}
|
|
||||||
} catch (ScriptException | IOException e) {
|
|
||||||
// ignore, user will notice the tray icon anyway.
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
final String title = localization.getString("tray.infoMsg.title");
|
|
||||||
final String msg = localization.getString("tray.infoMsg.msg");
|
|
||||||
notificationCmd = () -> {
|
|
||||||
trayIcon.displayMessage(title, msg, MessageType.INFO);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
SwingUtilities.invokeLater(() -> {
|
|
||||||
notificationCmd.run();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TrayIconMouseListener extends MouseAdapter {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void mouseClicked(MouseEvent e) {
|
|
||||||
if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
|
|
||||||
restoreFromTray(new ActionEvent(e.getSource(), e.getID(), e.paramString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void restoreFromTray(ActionEvent event) {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToForegroundApplication));
|
|
||||||
mainWindow.show();
|
|
||||||
mainWindow.requestFocus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateTrayIcon(boolean areAllVaultsLocked) {
|
|
||||||
if (trayIcon != null) {
|
|
||||||
Image image = getAppropriateTrayIconImage(areAllVaultsLocked);
|
|
||||||
trayIcon.setImage(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Image getAppropriateTrayIconImage(boolean areAllVaultsLocked) {
|
|
||||||
String resourceName;
|
|
||||||
if (SystemUtils.IS_OS_MAC_OSX && isMacMenuBarDarkMode()) {
|
|
||||||
resourceName = areAllVaultsLocked ? "/tray_icon_mac_white.png" : "/tray_icon_unlocked_mac_white.png";
|
|
||||||
} else if (SystemUtils.IS_OS_MAC_OSX) {
|
|
||||||
resourceName = areAllVaultsLocked ? "/tray_icon_mac_black.png" : "/tray_icon_unlocked_mac_black.png";
|
|
||||||
} else {
|
|
||||||
resourceName = areAllVaultsLocked ? "/tray_icon.png" : "/tray_icon_unlocked.png";
|
|
||||||
}
|
|
||||||
return Toolkit.getDefaultToolkit().getImage(getClass().getResource(resourceName));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,82 +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.ui;
|
|
||||||
|
|
||||||
import dagger.Module;
|
|
||||||
import dagger.Provides;
|
|
||||||
import javafx.beans.binding.Binding;
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.cryptomator.common.FxApplicationScoped;
|
|
||||||
import org.cryptomator.common.settings.Settings;
|
|
||||||
import org.cryptomator.frontend.webdav.WebDavServer;
|
|
||||||
import org.cryptomator.keychain.KeychainModule;
|
|
||||||
import org.cryptomator.ui.controllers.ViewControllerModule;
|
|
||||||
import org.cryptomator.ui.model.VaultComponent;
|
|
||||||
import org.fxmisc.easybind.EasyBind;
|
|
||||||
|
|
||||||
import javax.inject.Named;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
@Module(includes = {ViewControllerModule.class, KeychainModule.class}, subcomponents = {VaultComponent.class})
|
|
||||||
public class UiModule {
|
|
||||||
|
|
||||||
private static final int NUM_SCHEDULER_THREADS = 4;
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@FxApplicationScoped
|
|
||||||
ScheduledExecutorService provideScheduledExecutorService(@Named("shutdownTaskScheduler") Consumer<Runnable> shutdownTaskScheduler) {
|
|
||||||
final AtomicInteger threadNumber = new AtomicInteger(1);
|
|
||||||
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(NUM_SCHEDULER_THREADS, r -> {
|
|
||||||
Thread t = new Thread(r);
|
|
||||||
t.setName("Scheduler Thread " + threadNumber.getAndIncrement());
|
|
||||||
t.setDaemon(true);
|
|
||||||
return t;
|
|
||||||
});
|
|
||||||
shutdownTaskScheduler.accept(executorService::shutdown);
|
|
||||||
return executorService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@FxApplicationScoped
|
|
||||||
ExecutorService provideExecutorService(@Named("shutdownTaskScheduler") Consumer<Runnable> shutdownTaskScheduler) {
|
|
||||||
final AtomicInteger threadNumber = new AtomicInteger(1);
|
|
||||||
ExecutorService executorService = Executors.newCachedThreadPool(r -> {
|
|
||||||
Thread t = new Thread(r);
|
|
||||||
t.setName("Background Thread " + threadNumber.getAndIncrement());
|
|
||||||
t.setDaemon(true);
|
|
||||||
return t;
|
|
||||||
});
|
|
||||||
shutdownTaskScheduler.accept(executorService::shutdown);
|
|
||||||
return executorService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@FxApplicationScoped
|
|
||||||
Binding<InetSocketAddress> provideServerSocketAddressBinding(Settings settings) {
|
|
||||||
return EasyBind.map(settings.port(), (Number port) -> {
|
|
||||||
String host = SystemUtils.IS_OS_WINDOWS ? "127.0.0.1" : "localhost";
|
|
||||||
return InetSocketAddress.createUnresolved(host, port.intValue());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@FxApplicationScoped
|
|
||||||
WebDavServer provideWebDavServer(Binding<InetSocketAddress> serverSocketAddressBinding) {
|
|
||||||
WebDavServer server = WebDavServer.create();
|
|
||||||
// no need to unsubscribe eventually, because server is a singleton
|
|
||||||
EasyBind.subscribe(serverSocketAddressBinding, server::bind);
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package org.cryptomator.ui.addvaultwizard;
|
||||||
|
|
||||||
|
import dagger.Binds;
|
||||||
|
import dagger.Lazy;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import dagger.multibindings.IntoMap;
|
||||||
|
import dagger.multibindings.IntoSet;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyCodeCombination;
|
||||||
|
import javafx.scene.input.KeyCombination;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.ui.common.FXMLLoaderFactory;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.FxControllerKey;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
import org.cryptomator.ui.mainwindow.MainWindow;
|
||||||
|
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
public abstract class AddVaultModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@AddVaultWizardWindow
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
|
||||||
|
return new FXMLLoaderFactory(factories, resourceBundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@AddVaultWizardWindow
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon, @AddVaultWizardWindow Lazy<Map<KeyCodeCombination, Runnable>> accelerators) {
|
||||||
|
Stage stage = new Stage();
|
||||||
|
stage.setTitle(resourceBundle.getString("addvaultwizard.title"));
|
||||||
|
stage.setResizable(false);
|
||||||
|
stage.initModality(Modality.WINDOW_MODAL);
|
||||||
|
stage.initOwner(owner);
|
||||||
|
stage.sceneProperty().addListener(observable -> {
|
||||||
|
stage.getScene().getAccelerators().putAll(accelerators.get());
|
||||||
|
});
|
||||||
|
windowIcon.ifPresent(stage.getIcons()::add);
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
static ObjectProperty<Path> provideVaultPath() {
|
||||||
|
return new SimpleObjectProperty<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
static StringProperty provideVaultName() {
|
||||||
|
return new SimpleStringProperty("");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@AddVaultWizardWindow
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
static ObjectProperty<Vault> provideVault() {
|
||||||
|
return new SimpleObjectProperty<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@AddVaultWizardWindow
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
static Map<KeyCodeCombination, Runnable> provideDefaultAccellerators(@AddVaultWizardWindow Set<Map.Entry<KeyCombination, Runnable>> accelerators) {
|
||||||
|
return Map.ofEntries(accelerators.toArray(Map.Entry[]::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@IntoSet
|
||||||
|
@AddVaultWizardWindow
|
||||||
|
static Map.Entry<KeyCombination, Runnable> provideCloseWindowShortcut(@AddVaultWizardWindow Stage window) {
|
||||||
|
if (SystemUtils.IS_OS_WINDOWS) {
|
||||||
|
return Map.entry(new KeyCodeCombination(KeyCode.F4, KeyCombination.ALT_DOWN), window::close);
|
||||||
|
} else {
|
||||||
|
return Map.entry(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN), window::close);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@FxmlScene(FxmlFile.ADDVAULT_WELCOME)
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
static Scene provideWelcomeScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
|
||||||
|
Scene scene = fxmlLoaders.createScene("/fxml/addvault_welcome.fxml");
|
||||||
|
|
||||||
|
KeyCombination cmdW = new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN);
|
||||||
|
scene.getAccelerators().put(cmdW, window::close);
|
||||||
|
|
||||||
|
return scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@FxmlScene(FxmlFile.ADDVAULT_EXISTING)
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
static Scene provideChooseExistingVaultScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
|
||||||
|
return fxmlLoaders.createScene("/fxml/addvault_existing.fxml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@FxmlScene(FxmlFile.ADDVAULT_NEW_NAME)
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
static Scene provideCreateNewVaultNameScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
|
||||||
|
return fxmlLoaders.createScene("/fxml/addvault_new_name.fxml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION)
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
static Scene provideCreateNewVaultLocationScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
|
||||||
|
return fxmlLoaders.createScene("/fxml/addvault_new_location.fxml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD)
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
static Scene provideCreateNewVaultPasswordScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
|
||||||
|
return fxmlLoaders.createScene("/fxml/addvault_new_password.fxml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@FxmlScene(FxmlFile.ADDVAULT_SUCCESS)
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
static Scene provideCreateNewVaultSuccessScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
|
||||||
|
return fxmlLoaders.createScene("/fxml/addvault_success.fxml");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(AddVaultWelcomeController.class)
|
||||||
|
abstract FxController bindWelcomeController(AddVaultWelcomeController controller);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(ChooseExistingVaultController.class)
|
||||||
|
abstract FxController bindChooseExistingVaultController(ChooseExistingVaultController controller);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(CreateNewVaultNameController.class)
|
||||||
|
abstract FxController bindCreateNewVaultNameController(CreateNewVaultNameController controller);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(CreateNewVaultLocationController.class)
|
||||||
|
abstract FxController bindCreateNewVaultLocationController(CreateNewVaultLocationController controller);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(CreateNewVaultPasswordController.class)
|
||||||
|
abstract FxController bindCreateNewVaultPasswordController(CreateNewVaultPasswordController controller);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(AddVaultSuccessController.class)
|
||||||
|
abstract FxController bindAddVaultSuccessController(AddVaultSuccessController controller);
|
||||||
|
}
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
package org.cryptomator.ui.addvaultwizard;
|
||||||
|
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.fxapp.FxApplication;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
public class AddVaultSuccessController implements FxController {
|
||||||
|
|
||||||
|
private final FxApplication fxApplication;
|
||||||
|
private final Stage window;
|
||||||
|
private final ReadOnlyObjectProperty<Vault> vault;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AddVaultSuccessController(FxApplication fxApplication, @AddVaultWizardWindow Stage window, @AddVaultWizardWindow ObjectProperty<Vault> vault) {
|
||||||
|
this.fxApplication = fxApplication;
|
||||||
|
this.window = window;
|
||||||
|
this.vault = vault;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void unlockAndClose() {
|
||||||
|
close();
|
||||||
|
fxApplication.showUnlockWindow(vault.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void close() {
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Observables */
|
||||||
|
|
||||||
|
public ReadOnlyObjectProperty<Vault> vaultProperty() {
|
||||||
|
return vault;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vault getVault() {
|
||||||
|
return vault.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package org.cryptomator.ui.addvaultwizard;
|
||||||
|
|
||||||
|
import dagger.Lazy;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
public class AddVaultWelcomeController implements FxController {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(AddVaultWelcomeController.class);
|
||||||
|
private final Stage window;
|
||||||
|
private final Lazy<Scene> chooseExistingVaultScene;
|
||||||
|
private final Lazy<Scene> createNewVaultScene;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AddVaultWelcomeController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_EXISTING) Lazy<Scene> chooseExistingVaultScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> createNewVaultScene) {
|
||||||
|
this.window = window;
|
||||||
|
this.chooseExistingVaultScene = chooseExistingVaultScene;
|
||||||
|
this.createNewVaultScene = createNewVaultScene;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createNewVault() {
|
||||||
|
LOG.debug("AddVaultWelcomeController.createNewVault()");
|
||||||
|
window.setScene(createNewVaultScene.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void chooseExistingVault() {
|
||||||
|
LOG.debug("AddVaultWelcomeController.chooseExistingVault()");
|
||||||
|
window.setScene(chooseExistingVaultScene.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the accompanying LICENSE file.
|
||||||
|
*******************************************************************************/
|
||||||
|
package org.cryptomator.ui.addvaultwizard;
|
||||||
|
|
||||||
|
import dagger.Lazy;
|
||||||
|
import dagger.Subcomponent;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
@Subcomponent(modules = {AddVaultModule.class})
|
||||||
|
public interface AddVaultWizardComponent {
|
||||||
|
|
||||||
|
@AddVaultWizardWindow
|
||||||
|
Stage window();
|
||||||
|
|
||||||
|
@FxmlScene(FxmlFile.ADDVAULT_WELCOME)
|
||||||
|
Lazy<Scene> scene();
|
||||||
|
|
||||||
|
default void showAddVaultWizard() {
|
||||||
|
Stage stage = window();
|
||||||
|
stage.setScene(scene().get());
|
||||||
|
stage.sizeToScene();
|
||||||
|
stage.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subcomponent.Builder
|
||||||
|
interface Builder {
|
||||||
|
|
||||||
|
AddVaultWizardComponent build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.cryptomator.ui.addvaultwizard;
|
||||||
|
|
||||||
|
import javax.inject.Scope;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
@Scope
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@interface AddVaultWizardScoped {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.cryptomator.ui.addvaultwizard;
|
||||||
|
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Documented
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
@interface AddVaultWizardWindow {
|
||||||
|
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
package org.cryptomator.ui.addvaultwizard;
|
||||||
|
|
||||||
|
import dagger.Lazy;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.FileChooser;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.common.vaults.VaultListManager;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
public class ChooseExistingVaultController implements FxController {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ChooseExistingVaultController.class);
|
||||||
|
|
||||||
|
private final Stage window;
|
||||||
|
private final Lazy<Scene> welcomeScene;
|
||||||
|
private final Lazy<Scene> successScene;
|
||||||
|
private final ObjectProperty<Path> vaultPath;
|
||||||
|
private final ObjectProperty<Vault> vault;
|
||||||
|
private final VaultListManager vaultListManager;
|
||||||
|
private final ResourceBundle resourceBundle;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ChooseExistingVaultController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle) {
|
||||||
|
this.window = window;
|
||||||
|
this.welcomeScene = welcomeScene;
|
||||||
|
this.successScene = successScene;
|
||||||
|
this.vaultPath = vaultPath;
|
||||||
|
this.vault = vault;
|
||||||
|
this.vaultListManager = vaultListManager;
|
||||||
|
this.resourceBundle = resourceBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void back() {
|
||||||
|
window.setScene(welcomeScene.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void chooseFileAndNext() {
|
||||||
|
//TODO: error handling & cannot unlock added vault
|
||||||
|
FileChooser fileChooser = new FileChooser();
|
||||||
|
fileChooser.setTitle(resourceBundle.getString("addvaultwizard.existing.filePickerTitle"));
|
||||||
|
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
|
||||||
|
File file = fileChooser.showOpenDialog(window);
|
||||||
|
if (file != null) {
|
||||||
|
vaultPath.setValue(file.toPath().toAbsolutePath().getParent());
|
||||||
|
try {
|
||||||
|
Vault newVault = vaultListManager.add(vaultPath.get());
|
||||||
|
vault.set(newVault);
|
||||||
|
window.setScene(successScene.get());
|
||||||
|
} catch (NoSuchFileException e) {
|
||||||
|
LOG.error("Nope", e);
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+186
@@ -0,0 +1,186 @@
|
|||||||
|
package org.cryptomator.ui.addvaultwizard;
|
||||||
|
|
||||||
|
import dagger.Lazy;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.RadioButton;
|
||||||
|
import javafx.scene.control.Toggle;
|
||||||
|
import javafx.scene.control.ToggleGroup;
|
||||||
|
import javafx.stage.DirectoryChooser;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.DirectoryNotEmptyException;
|
||||||
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
public class CreateNewVaultLocationController implements FxController {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultLocationController.class);
|
||||||
|
private static final Path DEFAULT_CUSTOM_VAULT_PATH = Paths.get(System.getProperty("user.home"));
|
||||||
|
|
||||||
|
private final Stage window;
|
||||||
|
private final Lazy<Scene> chooseNameScene;
|
||||||
|
private final Lazy<Scene> choosePasswordScene;
|
||||||
|
private final LocationPresets locationPresets;
|
||||||
|
private final ObjectProperty<Path> vaultPath;
|
||||||
|
private final StringProperty vaultName;
|
||||||
|
private final ResourceBundle resourceBundle;
|
||||||
|
private final BooleanBinding validVaultPath;
|
||||||
|
private final BooleanBinding invalidVaultPath;
|
||||||
|
private final BooleanProperty usePresetPath;
|
||||||
|
private final StringProperty warningText;
|
||||||
|
|
||||||
|
private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
|
||||||
|
public ToggleGroup predefinedLocationToggler;
|
||||||
|
public RadioButton dropboxRadioButton;
|
||||||
|
public RadioButton gdriveRadioButton;
|
||||||
|
public RadioButton customRadioButton;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, LocationPresets locationPresets, ObjectProperty<Path> vaultPath, StringProperty vaultName, ResourceBundle resourceBundle) {
|
||||||
|
this.window = window;
|
||||||
|
this.chooseNameScene = chooseNameScene;
|
||||||
|
this.choosePasswordScene = choosePasswordScene;
|
||||||
|
this.locationPresets = locationPresets;
|
||||||
|
this.vaultPath = vaultPath;
|
||||||
|
this.vaultName = vaultName;
|
||||||
|
this.resourceBundle = resourceBundle;
|
||||||
|
this.validVaultPath = Bindings.createBooleanBinding(this::isValidVaultPath, vaultPath);
|
||||||
|
this.invalidVaultPath = validVaultPath.not();
|
||||||
|
this.usePresetPath = new SimpleBooleanProperty();
|
||||||
|
this.warningText = new SimpleStringProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidVaultPath() {
|
||||||
|
return vaultPath.get() != null && Files.notExists(vaultPath.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
|
||||||
|
usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
|
||||||
|
vaultPath.addListener(this::vaultPathDidChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void vaultPathDidChange(@SuppressWarnings("unused") ObservableValue<? extends Path> observable, @SuppressWarnings("unused") Path oldValue, Path newValue) {
|
||||||
|
if (!Files.notExists(newValue)) {
|
||||||
|
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
|
||||||
|
} else {
|
||||||
|
warningText.set(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
|
||||||
|
if (dropboxRadioButton.equals(newValue)) {
|
||||||
|
vaultPath.set(locationPresets.getDropboxLocation().resolve(vaultName.get()));
|
||||||
|
} else if (gdriveRadioButton.equals(newValue)) {
|
||||||
|
vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get()));
|
||||||
|
} else if (customRadioButton.equals(newValue)) {
|
||||||
|
vaultPath.set(customVaultPath.resolve(vaultName.get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void back() {
|
||||||
|
window.setScene(chooseNameScene.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void next() {
|
||||||
|
try {
|
||||||
|
// check if we have write access AND the vaultPath doesn't already exist:
|
||||||
|
assert Files.isDirectory(vaultPath.get().getParent());
|
||||||
|
Path createdDir = Files.createDirectory(vaultPath.get());
|
||||||
|
Files.delete(createdDir); // assert: dir exists and is empty
|
||||||
|
window.setScene(choosePasswordScene.get());
|
||||||
|
} catch (FileAlreadyExistsException e) {
|
||||||
|
LOG.warn("Can not use already existing vault path: {}", vaultPath.get());
|
||||||
|
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
|
||||||
|
} catch (NoSuchFileException | DirectoryNotEmptyException e) {
|
||||||
|
LOG.error("Failed to delete recently created directory.", e);
|
||||||
|
// TODO show generic error text for unexpected exception
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.warn("Can not create vault at path: {}", vaultPath.get());
|
||||||
|
// TODO show generic error text for unexpected exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void chooseCustomVaultPath() {
|
||||||
|
DirectoryChooser directoryChooser = new DirectoryChooser();
|
||||||
|
directoryChooser.setTitle(resourceBundle.getString("addvaultwizard.new.directoryPickerTitle"));
|
||||||
|
directoryChooser.setInitialDirectory(customVaultPath.toFile());
|
||||||
|
final File file = directoryChooser.showDialog(window);
|
||||||
|
if (file != null) {
|
||||||
|
customVaultPath = file.toPath().toAbsolutePath();
|
||||||
|
vaultPath.set(customVaultPath.resolve(vaultName.get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Getter/Setter */
|
||||||
|
|
||||||
|
public Path getVaultPath() {
|
||||||
|
return vaultPath.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<Path> vaultPathProperty() {
|
||||||
|
return vaultPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding invalidVaultPathProperty() {
|
||||||
|
return invalidVaultPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getInvalidVaultPath() {
|
||||||
|
return invalidVaultPath.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocationPresets getLocationPresets() {
|
||||||
|
return locationPresets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanProperty usePresetPathProperty() {
|
||||||
|
return usePresetPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getUsePresetPath() {
|
||||||
|
return usePresetPath.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty warningTextProperty() {
|
||||||
|
return warningText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWarningText() {
|
||||||
|
return warningText.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding showWarningProperty() {
|
||||||
|
return warningText.isNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isShowWarning() {
|
||||||
|
return showWarningProperty().get();
|
||||||
|
}
|
||||||
|
}
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
package org.cryptomator.ui.addvaultwizard;
|
||||||
|
|
||||||
|
import dagger.Lazy;
|
||||||
|
import javafx.beans.Observable;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.binding.StringBinding;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
public class CreateNewVaultNameController implements FxController {
|
||||||
|
|
||||||
|
private static final Pattern VALID_NAME_PATTERN = Pattern.compile("[\\w -]+", Pattern.UNICODE_CHARACTER_CLASS);
|
||||||
|
|
||||||
|
public TextField textField;
|
||||||
|
private final Stage window;
|
||||||
|
private final Lazy<Scene> welcomeScene;
|
||||||
|
private final Lazy<Scene> chooseLocationScene;
|
||||||
|
private final ObjectProperty<Path> vaultPath;
|
||||||
|
private final StringProperty vaultName;
|
||||||
|
private final BooleanBinding validVaultName;
|
||||||
|
private final BooleanBinding invalidVaultName;
|
||||||
|
private final StringBinding warningText;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CreateNewVaultNameController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, ObjectProperty<Path> vaultPath, StringProperty vaultName, ResourceBundle resourceBundle) {
|
||||||
|
this.window = window;
|
||||||
|
this.welcomeScene = welcomeScene;
|
||||||
|
this.chooseLocationScene = chooseLocationScene;
|
||||||
|
this.vaultPath = vaultPath;
|
||||||
|
this.vaultName = vaultName;
|
||||||
|
this.validVaultName = Bindings.createBooleanBinding(this::isValidVaultName, vaultName);
|
||||||
|
this.invalidVaultName = validVaultName.not();
|
||||||
|
this.warningText = Bindings.when(vaultName.isNotEmpty().and(invalidVaultName)).then(resourceBundle.getString("addvaultwizard.new.invalidName")).otherwise((String) null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
vaultName.bind(textField.textProperty());
|
||||||
|
vaultName.addListener(this::vaultNameChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValidVaultName() {
|
||||||
|
return vaultName.get() != null && VALID_NAME_PATTERN.matcher(vaultName.get().trim()).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void vaultNameChanged(@SuppressWarnings("unused") Observable observable) {
|
||||||
|
if (isValidVaultName()) {
|
||||||
|
if (vaultPath.get() != null) {
|
||||||
|
// update vaultPath if it is already set but the user went back to change its name:
|
||||||
|
vaultPath.set(vaultPath.get().resolveSibling(vaultName.get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void back() {
|
||||||
|
window.setScene(welcomeScene.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void next() {
|
||||||
|
window.setScene(chooseLocationScene.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Getter/Setter */
|
||||||
|
|
||||||
|
public BooleanBinding invalidVaultNameProperty() {
|
||||||
|
return invalidVaultName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInvalidVaultName() {
|
||||||
|
return invalidVaultName.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringBinding warningTextProperty() {
|
||||||
|
return warningText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWarningText() {
|
||||||
|
return warningText.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding showWarningProperty() {
|
||||||
|
return warningText.isNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isShowWarning() {
|
||||||
|
return showWarningProperty().get();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+217
@@ -0,0 +1,217 @@
|
|||||||
|
package org.cryptomator.ui.addvaultwizard;
|
||||||
|
|
||||||
|
import dagger.Lazy;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.binding.ObjectBinding;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.ContentDisplay;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.common.vaults.VaultListManager;
|
||||||
|
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
||||||
|
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
import org.cryptomator.ui.common.Tasks;
|
||||||
|
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||||
|
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
||||||
|
import org.cryptomator.ui.common.PasswordStrengthUtil;
|
||||||
|
import org.fxmisc.easybind.EasyBind;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.nio.channels.WritableByteChannel;
|
||||||
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||||
|
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
public class CreateNewVaultPasswordController implements FxController {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultPasswordController.class);
|
||||||
|
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
|
||||||
|
|
||||||
|
private final Stage window;
|
||||||
|
private final Lazy<Scene> chooseLocationScene;
|
||||||
|
private final Lazy<Scene> successScene;
|
||||||
|
private final ExecutorService executor;
|
||||||
|
private final StringProperty vaultName;
|
||||||
|
private final ObjectProperty<Path> vaultPath;
|
||||||
|
private final ObjectProperty<Vault> vault;
|
||||||
|
private final VaultListManager vaultListManager;
|
||||||
|
private final ResourceBundle resourceBundle;
|
||||||
|
private final PasswordStrengthUtil strengthRater;
|
||||||
|
private final ReadmeGenerator readmeGenerator;
|
||||||
|
private final IntegerProperty passwordStrength;
|
||||||
|
private final BooleanProperty processing;
|
||||||
|
private final BooleanProperty readyToCreateVault;
|
||||||
|
private final ObjectBinding<ContentDisplay> createVaultButtonState;
|
||||||
|
|
||||||
|
public NiceSecurePasswordField passwordField;
|
||||||
|
public NiceSecurePasswordField reenterField;
|
||||||
|
public Label passwordStrengthLabel;
|
||||||
|
public HBox passwordMatchBox;
|
||||||
|
public FontAwesome5IconView checkmark;
|
||||||
|
public FontAwesome5IconView cross;
|
||||||
|
public Label passwordMatchLabel;
|
||||||
|
public CheckBox finalConfirmationCheckbox;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ExecutorService executor, StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, ReadmeGenerator readmeGenerator) {
|
||||||
|
this.window = window;
|
||||||
|
this.chooseLocationScene = chooseLocationScene;
|
||||||
|
this.successScene = successScene;
|
||||||
|
this.executor = executor;
|
||||||
|
this.vaultName = vaultName;
|
||||||
|
this.vaultPath = vaultPath;
|
||||||
|
this.vault = vault;
|
||||||
|
this.vaultListManager = vaultListManager;
|
||||||
|
this.resourceBundle = resourceBundle;
|
||||||
|
this.strengthRater = strengthRater;
|
||||||
|
this.readmeGenerator = readmeGenerator;
|
||||||
|
this.passwordStrength = new SimpleIntegerProperty(-1);
|
||||||
|
this.processing = new SimpleBooleanProperty();
|
||||||
|
this.readyToCreateVault = new SimpleBooleanProperty();
|
||||||
|
this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
//binds the actual strength value to the rating of the password util
|
||||||
|
passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(passwordField.getCharacters().toString()), passwordField.textProperty()));
|
||||||
|
//binding indicating if the passwords not match
|
||||||
|
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(() -> CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0, passwordField.textProperty(), reenterField.textProperty());
|
||||||
|
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
|
||||||
|
readyToCreateVault.bind(reenterFieldNotEmpty.and(passwordsMatch).and(finalConfirmationCheckbox.selectedProperty()).and(processing.not()));
|
||||||
|
//make match indicator invisible when passwords do not match or one is empty
|
||||||
|
passwordMatchBox.visibleProperty().bind(reenterFieldNotEmpty);
|
||||||
|
checkmark.visibleProperty().bind(passwordsMatch.and(reenterFieldNotEmpty));
|
||||||
|
checkmark.managedProperty().bind(checkmark.visibleProperty());
|
||||||
|
cross.visibleProperty().bind(passwordsMatch.not().and(reenterFieldNotEmpty));
|
||||||
|
cross.managedProperty().bind(cross.visibleProperty());
|
||||||
|
passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("addvaultwizard.new.passwordsMatch")).otherwise(resourceBundle.getString("addvaultwizard.new.passwordsDoNotMatch")));
|
||||||
|
|
||||||
|
//bindsings for the password strength indicator
|
||||||
|
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void back() {
|
||||||
|
window.setScene(chooseLocationScene.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void next() {
|
||||||
|
Path pathToVault = vaultPath.get();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Files.createDirectory(pathToVault);
|
||||||
|
} catch (FileAlreadyExistsException e) {
|
||||||
|
LOG.error("Vault dir already exists.", e);
|
||||||
|
window.setScene(chooseLocationScene.get());
|
||||||
|
} catch (IOException e) {
|
||||||
|
// TODO show generic error screen
|
||||||
|
LOG.error("", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
processing.set(true);
|
||||||
|
Tasks.create(() -> {
|
||||||
|
initializeVault(pathToVault, passwordField.getCharacters());
|
||||||
|
}).onSuccess(() -> {
|
||||||
|
initializationSucceeded(pathToVault);
|
||||||
|
}).onError(IOException.class, e -> {
|
||||||
|
// TODO show generic error screen
|
||||||
|
LOG.error("", e);
|
||||||
|
}).andFinally(() -> {
|
||||||
|
processing.set(false);
|
||||||
|
}).runOnce(executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeVault(Path path, CharSequence passphrase) throws IOException {
|
||||||
|
CryptoFileSystemProvider.initialize(path, MASTERKEY_FILENAME, passphrase);
|
||||||
|
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
||||||
|
.withPassphrase(passphrase) //
|
||||||
|
.withFlags(Collections.emptySet()) //
|
||||||
|
.withMasterkeyFilename(MASTERKEY_FILENAME) //
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
|
||||||
|
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
|
||||||
|
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||||
|
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
|
||||||
|
}
|
||||||
|
|
||||||
|
String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName");
|
||||||
|
try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||||
|
ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf()));
|
||||||
|
}
|
||||||
|
LOG.info("Created vault at {}", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializationSucceeded(Path pathToVault) {
|
||||||
|
try {
|
||||||
|
Vault newVault = vaultListManager.add(pathToVault);
|
||||||
|
vault.set(newVault);
|
||||||
|
window.setScene(successScene.get());
|
||||||
|
} catch (NoSuchFileException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Getter/Setter */
|
||||||
|
|
||||||
|
public String getVaultName() {
|
||||||
|
return vaultName.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty vaultNameProperty() {
|
||||||
|
return vaultName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IntegerProperty passwordStrengthProperty() {
|
||||||
|
return passwordStrength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPasswordStrength() {
|
||||||
|
return passwordStrength.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanProperty readyToCreateVaultProperty() {
|
||||||
|
return readyToCreateVault;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReadyToCreateVault() {
|
||||||
|
return readyToCreateVault.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectBinding<ContentDisplay> createVaultButtonStateProperty() {
|
||||||
|
return createVaultButtonState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContentDisplay getCreateVaultButtonState() {
|
||||||
|
return processing.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package org.cryptomator.ui.addvaultwizard;
|
||||||
|
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
public class LocationPresets {
|
||||||
|
|
||||||
|
private static final String USER_HOME = System.getProperty("user.home");
|
||||||
|
private static final String[] DROPBOX_LOCATIONS = {"~/Dropbox"};
|
||||||
|
private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive"};
|
||||||
|
|
||||||
|
private final ReadOnlyObjectProperty<Path> dropboxLocation;
|
||||||
|
private final ReadOnlyObjectProperty<Path> gdriveLocation;
|
||||||
|
private final BooleanBinding foundDropbox;
|
||||||
|
private final BooleanBinding foundGdrive;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public LocationPresets() {
|
||||||
|
this.dropboxLocation = new SimpleObjectProperty<>(existingWritablePath(DROPBOX_LOCATIONS));
|
||||||
|
this.gdriveLocation = new SimpleObjectProperty<>(existingWritablePath(GDRIVE_LOCATIONS));
|
||||||
|
this.foundDropbox = dropboxLocation.isNotNull();
|
||||||
|
this.foundGdrive = gdriveLocation.isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path existingWritablePath(String... candidates) {
|
||||||
|
for (String candidate : candidates) {
|
||||||
|
Path path = Paths.get(resolveHomePath(candidate));
|
||||||
|
if (Files.isDirectory(path)) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolveHomePath(String path) {
|
||||||
|
if (path.startsWith("~/")) {
|
||||||
|
return USER_HOME + path.substring(1);
|
||||||
|
} else {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Observables */
|
||||||
|
|
||||||
|
public ReadOnlyObjectProperty<Path> dropboxLocationProperty() {
|
||||||
|
return dropboxLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getDropboxLocation() {
|
||||||
|
return dropboxLocation.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding foundDropboxProperty() {
|
||||||
|
return foundDropbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFoundDropbox() {
|
||||||
|
return foundDropbox.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlyObjectProperty<Path> gdriveLocationProperty() {
|
||||||
|
return gdriveLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getGdriveLocation() {
|
||||||
|
return gdriveLocation.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding froundGdriveProperty() {
|
||||||
|
return foundGdrive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFoundGdrive() {
|
||||||
|
return foundGdrive.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package org.cryptomator.ui.addvaultwizard;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
@AddVaultWizardScoped
|
||||||
|
public class ReadmeGenerator {
|
||||||
|
|
||||||
|
// specs: https://web.archive.org/web/20190708132914/http://www.kleinlercher.at/tools/Windows_Protocols/Word2007RTFSpec9.pdf
|
||||||
|
private static final String RTF_HEADER = "{\\rtf1\\fbidis\\ansi\\uc0\\fs32\n";
|
||||||
|
private static final String RTF_FOOTER = "}";
|
||||||
|
private static final String HELP_URL = "{\\field{\\*\\fldinst HYPERLINK \"http://www.google.com/\"}{\\fldrslt google.com}}";
|
||||||
|
|
||||||
|
private final ResourceBundle resourceBundle;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ReadmeGenerator(ResourceBundle resourceBundle){
|
||||||
|
this.resourceBundle = resourceBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String createVaultStorageLocationReadmeRtf() {
|
||||||
|
return createDocument(List.of( //
|
||||||
|
resourceBundle.getString("addvault.new.readme.storageLocation.1"), //
|
||||||
|
resourceBundle.getString("addvault.new.readme.storageLocation.2"), //
|
||||||
|
resourceBundle.getString("addvault.new.readme.storageLocation.3"), //
|
||||||
|
String.format(resourceBundle.getString("addvault.new.readme.storageLocation.4"), HELP_URL) //
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String createVaultAccessLocationReadmeRtf() {
|
||||||
|
return createDocument(List.of( //
|
||||||
|
resourceBundle.getString("addvault.new.readme.accessLocation.1"), //
|
||||||
|
resourceBundle.getString("addvault.new.readme.accessLocation.2"), //
|
||||||
|
resourceBundle.getString("addvault.new.readme.accessLocation.3") //
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// visible for testing
|
||||||
|
String createDocument(Iterable<String> paragraphs) {
|
||||||
|
StringBuilder sb = new StringBuilder(RTF_HEADER);
|
||||||
|
for (String p : paragraphs) {
|
||||||
|
sb.append("\\par {\\sa80 ");
|
||||||
|
appendEscaped(sb, p);
|
||||||
|
sb.append("}\n");
|
||||||
|
}
|
||||||
|
sb.append(RTF_FOOTER);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// visible for testing
|
||||||
|
String escapeNonAsciiChars(CharSequence input) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
appendEscaped(sb, input);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendEscaped(StringBuilder sb, CharSequence input) {
|
||||||
|
input.chars().forEachOrdered(c -> {
|
||||||
|
if (c < 128) {
|
||||||
|
sb.append((char) c);
|
||||||
|
} else if (c < 0xFFFF) {
|
||||||
|
sb.append("\\u").append(c);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.cryptomator.ui.changepassword;
|
||||||
|
|
||||||
|
import dagger.BindsInstance;
|
||||||
|
import dagger.Lazy;
|
||||||
|
import dagger.Subcomponent;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
|
@ChangePasswordScoped
|
||||||
|
@Subcomponent(modules = {ChangePasswordModule.class})
|
||||||
|
public interface ChangePasswordComponent {
|
||||||
|
|
||||||
|
@ChangePasswordWindow
|
||||||
|
Stage window();
|
||||||
|
|
||||||
|
@FxmlScene(FxmlFile.CHANGEPASSWORD)
|
||||||
|
Lazy<Scene> scene();
|
||||||
|
|
||||||
|
default void showChangePasswordWindow() {
|
||||||
|
Stage stage = window();
|
||||||
|
stage.setScene(scene().get());
|
||||||
|
stage.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subcomponent.Builder
|
||||||
|
interface Builder {
|
||||||
|
|
||||||
|
@BindsInstance
|
||||||
|
Builder vault(@ChangePasswordWindow Vault vault);
|
||||||
|
|
||||||
|
@BindsInstance
|
||||||
|
Builder owner(@Named("changePasswordOwner") Stage owner);
|
||||||
|
|
||||||
|
ChangePasswordComponent build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
package org.cryptomator.ui.changepassword;
|
||||||
|
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||||
|
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||||
|
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
||||||
|
import org.cryptomator.ui.common.PasswordStrengthUtil;
|
||||||
|
import org.fxmisc.easybind.EasyBind;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
@ChangePasswordScoped
|
||||||
|
public class ChangePasswordController implements FxController {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class);
|
||||||
|
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
|
||||||
|
|
||||||
|
private final Stage window;
|
||||||
|
private final Vault vault;
|
||||||
|
private final ResourceBundle resourceBundle;
|
||||||
|
private final PasswordStrengthUtil strengthRater;
|
||||||
|
private final IntegerProperty passwordStrength;
|
||||||
|
|
||||||
|
public NiceSecurePasswordField oldPasswordField;
|
||||||
|
public NiceSecurePasswordField newPasswordField;
|
||||||
|
public NiceSecurePasswordField reenterPasswordField;
|
||||||
|
public Label passwordStrengthLabel;
|
||||||
|
public HBox passwordMatchBox;
|
||||||
|
public FontAwesome5IconView checkmark;
|
||||||
|
public FontAwesome5IconView cross;
|
||||||
|
public Label passwordMatchLabel;
|
||||||
|
public CheckBox finalConfirmationCheckbox;
|
||||||
|
public Button finishButton;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
|
||||||
|
this.window = window;
|
||||||
|
this.vault = vault;
|
||||||
|
this.resourceBundle = resourceBundle;
|
||||||
|
this.strengthRater = strengthRater;
|
||||||
|
this.passwordStrength = new SimpleIntegerProperty(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
//binds the actual strength value to the rating of the password util
|
||||||
|
passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(newPasswordField.getCharacters().toString()), newPasswordField.textProperty()));
|
||||||
|
//binding indicating if the passwords not match
|
||||||
|
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(() -> CharSequence.compare(newPasswordField.getCharacters(), reenterPasswordField.getCharacters()) == 0, newPasswordField.textProperty(), reenterPasswordField.textProperty());
|
||||||
|
BooleanBinding reenterFieldNotEmpty = reenterPasswordField.textProperty().isNotEmpty();
|
||||||
|
//disable the finish button when passwords do not match or one is empty
|
||||||
|
finishButton.disableProperty().bind(reenterFieldNotEmpty.not().or(passwordsMatch.not()).or(finalConfirmationCheckbox.selectedProperty().not()));
|
||||||
|
//make match indicator invisible when passwords do not match or one is empty
|
||||||
|
passwordMatchBox.visibleProperty().bind(reenterFieldNotEmpty);
|
||||||
|
checkmark.visibleProperty().bind(passwordsMatch.and(reenterFieldNotEmpty));
|
||||||
|
checkmark.managedProperty().bind(checkmark.visibleProperty());
|
||||||
|
cross.visibleProperty().bind(passwordsMatch.not().and(reenterFieldNotEmpty));
|
||||||
|
cross.managedProperty().bind(cross.visibleProperty());
|
||||||
|
passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("changepassword.passwordsMatch")).otherwise(resourceBundle.getString("changepassword.passwordsDoNotMatch")));
|
||||||
|
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void cancel() {
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void finish() {
|
||||||
|
try {
|
||||||
|
CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPasswordField.getCharacters());
|
||||||
|
LOG.info("Successful changed password for {}", vault.getDisplayableName());
|
||||||
|
window.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
//TODO
|
||||||
|
LOG.error("IO error occured during password change. Unable to perform operation.", e);
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (InvalidPassphraseException e) {
|
||||||
|
//TODO
|
||||||
|
LOG.info("Wrong old password.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Getter/Setter */
|
||||||
|
|
||||||
|
public Vault getVault() {
|
||||||
|
return vault;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IntegerProperty passwordStrengthProperty() {
|
||||||
|
return passwordStrength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPasswordStrength() {
|
||||||
|
return passwordStrength.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package org.cryptomator.ui.changepassword;
|
||||||
|
|
||||||
|
import dagger.Binds;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import dagger.multibindings.IntoMap;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.cryptomator.ui.common.FXMLLoaderFactory;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.FxControllerKey;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
abstract class ChangePasswordModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@ChangePasswordWindow
|
||||||
|
@ChangePasswordScoped
|
||||||
|
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
|
||||||
|
return new FXMLLoaderFactory(factories, resourceBundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@ChangePasswordWindow
|
||||||
|
@ChangePasswordScoped
|
||||||
|
static Stage provideStage(@Named("changePasswordOwner") Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon) {
|
||||||
|
Stage stage = new Stage();
|
||||||
|
stage.setTitle(resourceBundle.getString("changepassword.title"));
|
||||||
|
stage.setResizable(false);
|
||||||
|
stage.initModality(Modality.WINDOW_MODAL);
|
||||||
|
stage.initOwner(owner);
|
||||||
|
windowIcon.ifPresent(stage.getIcons()::add);
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@FxmlScene(FxmlFile.CHANGEPASSWORD)
|
||||||
|
@ChangePasswordScoped
|
||||||
|
static Scene provideUnlockScene(@ChangePasswordWindow FXMLLoaderFactory fxmlLoaders) {
|
||||||
|
return fxmlLoaders.createScene("/fxml/changepassword.fxml");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ------------------
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(ChangePasswordController.class)
|
||||||
|
abstract FxController bindUnlockController(ChangePasswordController controller);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.cryptomator.ui.changepassword;
|
||||||
|
|
||||||
|
import javax.inject.Scope;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
@Scope
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@interface ChangePasswordScoped {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.cryptomator.ui.changepassword;
|
||||||
|
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Documented
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
@interface ChangePasswordWindow {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package org.cryptomator.ui.common;
|
||||||
|
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Parent;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
public class FXMLLoaderFactory {
|
||||||
|
|
||||||
|
private final Map<Class<? extends FxController>, Provider<FxController>> factories;
|
||||||
|
private final ResourceBundle resourceBundle;
|
||||||
|
|
||||||
|
public FXMLLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
|
||||||
|
this.factories = factories;
|
||||||
|
this.resourceBundle = resourceBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A new FXMLLoader instance
|
||||||
|
*/
|
||||||
|
public FXMLLoader construct() {
|
||||||
|
FXMLLoader loader = new FXMLLoader();
|
||||||
|
loader.setControllerFactory(this::constructController);
|
||||||
|
loader.setResources(resourceBundle);
|
||||||
|
return loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the FXML given fxml resource in a new FXMLLoader instance.
|
||||||
|
* @param fxmlResourceName Name of the resource (as in {@link Class#getResource(String)}).
|
||||||
|
* @return The FXMLLoader used to load the file
|
||||||
|
* @throws IOException if an error occurs while loading the FXML file
|
||||||
|
*/
|
||||||
|
public FXMLLoader load(String fxmlResourceName) throws IOException {
|
||||||
|
FXMLLoader loader = construct();
|
||||||
|
try (InputStream in = getClass().getResourceAsStream(fxmlResourceName)) {
|
||||||
|
loader.load(in);
|
||||||
|
}
|
||||||
|
return loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link #load(String) Loads} the FXML file and creates a new Scene containing the loaded ui.
|
||||||
|
* @param fxmlResourceName Name of the resource (as in {@link Class#getResource(String)}).
|
||||||
|
* @throws UncheckedIOException wrapping any IOException thrown by {@link #load(String)).
|
||||||
|
*/
|
||||||
|
public Scene createScene(String fxmlResourceName) {
|
||||||
|
final FXMLLoader loader;
|
||||||
|
try {
|
||||||
|
loader = load(fxmlResourceName);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException("Failed to load " + fxmlResourceName, e);
|
||||||
|
}
|
||||||
|
Parent root = loader.getRoot();
|
||||||
|
return new Scene(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FxController constructController(Class<?> aClass) {
|
||||||
|
if (!factories.containsKey(aClass)) {
|
||||||
|
throw new IllegalArgumentException("ViewController not registered: " + aClass);
|
||||||
|
} else {
|
||||||
|
return factories.get(aClass).get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package org.cryptomator.ui.common;
|
||||||
|
|
||||||
|
import javafx.scene.text.Font;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class FontLoader {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(FontLoader.class);
|
||||||
|
private static final double DEFAULT_FONT_SIZE = 12;
|
||||||
|
|
||||||
|
public static Font load(String resourcePath) throws FontLoaderException {
|
||||||
|
try (InputStream in = FontLoader.class.getResourceAsStream(resourcePath)) {
|
||||||
|
if (in == null) {
|
||||||
|
throw new FontLoaderException(resourcePath);
|
||||||
|
} else {
|
||||||
|
return load(resourcePath, in);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FontLoaderException(resourcePath, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Font load(String resourcePath, InputStream in) throws FontLoaderException {
|
||||||
|
Font font = Font.loadFont(in, DEFAULT_FONT_SIZE);
|
||||||
|
if (font != null) {
|
||||||
|
LOG.debug("Loaded family: {}", font.getFamily());
|
||||||
|
return font;
|
||||||
|
} else {
|
||||||
|
throw new FontLoaderException(resourcePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FontLoaderException extends IOException {
|
||||||
|
|
||||||
|
private FontLoaderException(String resourceName) {
|
||||||
|
super("Failed to load font: " + resourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FontLoaderException(String resourceName, Throwable cause) {
|
||||||
|
super("Failed to load font: " + resourceName, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the accompanying LICENSE file.
|
||||||
|
*******************************************************************************/
|
||||||
|
package org.cryptomator.ui.common;
|
||||||
|
|
||||||
|
public interface FxController {
|
||||||
|
}
|
||||||
+7
-6
@@ -3,21 +3,22 @@
|
|||||||
* All rights reserved. This program and the accompanying materials
|
* All rights reserved. This program and the accompanying materials
|
||||||
* are made available under the terms of the accompanying LICENSE file.
|
* are made available under the terms of the accompanying LICENSE file.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.ui.controllers;
|
package org.cryptomator.ui.common;
|
||||||
|
|
||||||
import static java.lang.annotation.ElementType.METHOD;
|
import dagger.MapKey;
|
||||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
|
||||||
|
|
||||||
import java.lang.annotation.Documented;
|
import java.lang.annotation.Documented;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
import dagger.MapKey;
|
import static java.lang.annotation.ElementType.METHOD;
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
// TODO rename after refactoring
|
||||||
@Documented
|
@Documented
|
||||||
@Target(METHOD)
|
@Target(METHOD)
|
||||||
@Retention(RUNTIME)
|
@Retention(RUNTIME)
|
||||||
@MapKey
|
@MapKey
|
||||||
public @interface ViewControllerKey {
|
public @interface FxControllerKey {
|
||||||
Class<? extends ViewController> value();
|
Class<? extends FxController> value();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.cryptomator.ui.common;
|
||||||
|
|
||||||
|
public enum FxmlFile {
|
||||||
|
ADDVAULT_WELCOME("/fxml/addvault_welcome.fxml"), //
|
||||||
|
ADDVAULT_EXISTING("/fxml/addvault_existing.fxml"), //
|
||||||
|
ADDVAULT_NEW_NAME("/fxml/addvault_new_name.fxml"), //
|
||||||
|
ADDVAULT_NEW_LOCATION("/fxml/addvault_new_location.fxml"), //
|
||||||
|
ADDVAULT_NEW_PASSWORD("/fxml/addvault_new_password.fxml"), //
|
||||||
|
ADDVAULT_SUCCESS("/fxml/addvault_success.fxml"), //
|
||||||
|
CHANGEPASSWORD("/fxml/changepassword.fxml"), //
|
||||||
|
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
|
||||||
|
MAIN_WINDOW("/fxml/main_window.fxml"), //
|
||||||
|
MIGRATION_RUN("/fxml/migration_run.fxml"), //
|
||||||
|
MIGRATION_START("/fxml/migration_start.fxml"), //
|
||||||
|
MIGRATION_SUCCESS("/fxml/migration_success.fxml"), //
|
||||||
|
PREFERENCES("/fxml/preferences.fxml"), //
|
||||||
|
QUIT("/fxml/quit.fxml"), //
|
||||||
|
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
|
||||||
|
UNLOCK("/fxml/unlock2.fxml"), // TODO rename
|
||||||
|
UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
|
||||||
|
VAULT_OPTIONS("/fxml/vault_options.fxml"), //
|
||||||
|
WRONGFILEALERT("/fxml/wrongfilealert.fxml");
|
||||||
|
|
||||||
|
private final String filename;
|
||||||
|
|
||||||
|
FxmlFile(String filename) {
|
||||||
|
this.filename = filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.cryptomator.ui.common;
|
||||||
|
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Documented
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
public @interface FxmlScene {
|
||||||
|
|
||||||
|
FxmlFile value();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* 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:
|
||||||
|
* Jean-Noël Charon - initial API and implementation
|
||||||
|
*******************************************************************************/
|
||||||
|
package org.cryptomator.ui.common;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.nulabinc.zxcvbn.Zxcvbn;
|
||||||
|
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
@FxApplicationScoped
|
||||||
|
public class PasswordStrengthUtil {
|
||||||
|
|
||||||
|
private static final int PW_TRUNC_LEN = 100; // truncate very long passwords, since zxcvbn memory and runtime depends vastly on the length
|
||||||
|
private static final String RESSOURCE_PREFIX = "passwordStrength.messageLabel.";
|
||||||
|
|
||||||
|
private final Zxcvbn zxcvbn;
|
||||||
|
private final List<String> sanitizedInputs;
|
||||||
|
private final ResourceBundle resourceBundle;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public PasswordStrengthUtil(ResourceBundle resourceBundle) {
|
||||||
|
this.resourceBundle = resourceBundle;
|
||||||
|
this.zxcvbn = new Zxcvbn();
|
||||||
|
this.sanitizedInputs = new ArrayList<>();
|
||||||
|
this.sanitizedInputs.add("cryptomator");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int computeRate(String password) {
|
||||||
|
if (Strings.isNullOrEmpty(password)) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
int numCharsToRate = Math.min(PW_TRUNC_LEN, password.length());
|
||||||
|
return zxcvbn.measure(password.substring(0, numCharsToRate), sanitizedInputs).getScore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStrengthDescription(Number score) {
|
||||||
|
if (resourceBundle.containsKey(RESSOURCE_PREFIX + score.intValue())) {
|
||||||
|
return resourceBundle.getString("passwordStrength.messageLabel." + score.intValue());
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
* All rights reserved. This program and the accompanying materials
|
* All rights reserved. This program and the accompanying materials
|
||||||
* are made available under the terms of the accompanying LICENSE file.
|
* are made available under the terms of the accompanying LICENSE file.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.ui.util;
|
package org.cryptomator.ui.common;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -1,203 +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
|
|
||||||
* Jean-Noël Charon - password strength meter
|
|
||||||
*******************************************************************************/
|
|
||||||
package org.cryptomator.ui.controllers;
|
|
||||||
|
|
||||||
import javafx.application.Application;
|
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.beans.Observable;
|
|
||||||
import javafx.beans.property.IntegerProperty;
|
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.fxml.FXML;
|
|
||||||
import javafx.scene.Parent;
|
|
||||||
import javafx.scene.control.Button;
|
|
||||||
import javafx.scene.control.Hyperlink;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.layout.GridPane;
|
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
import javafx.scene.text.Text;
|
|
||||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
|
||||||
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
|
|
||||||
import org.cryptomator.ui.controls.SecPasswordField;
|
|
||||||
import org.cryptomator.ui.l10n.Localization;
|
|
||||||
import org.cryptomator.ui.model.Vault;
|
|
||||||
import org.cryptomator.ui.util.PasswordStrengthUtil;
|
|
||||||
import org.fxmisc.easybind.EasyBind;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.UncheckedIOException;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class ChangePasswordController implements ViewController {
|
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class);
|
|
||||||
|
|
||||||
private final Application app;
|
|
||||||
private final PasswordStrengthUtil strengthRater;
|
|
||||||
private final Localization localization;
|
|
||||||
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1); // 0-4
|
|
||||||
private Optional<ChangePasswordListener> listener = Optional.empty();
|
|
||||||
private Vault vault;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public ChangePasswordController(Application app, PasswordStrengthUtil strengthRater, Localization localization) {
|
|
||||||
this.app = app;
|
|
||||||
this.strengthRater = strengthRater;
|
|
||||||
this.localization = localization;
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private SecPasswordField oldPasswordField;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private SecPasswordField newPasswordField;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private SecPasswordField retypePasswordField;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Button changePasswordButton;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Text messageText;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Hyperlink downloadsPageLink;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label passwordStrengthLabel;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Region passwordStrengthLevel0;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Region passwordStrengthLevel1;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Region passwordStrengthLevel2;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Region passwordStrengthLevel3;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Region passwordStrengthLevel4;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private GridPane root;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize() {
|
|
||||||
oldPasswordField.textProperty().addListener(this::passwordsChanged);
|
|
||||||
newPasswordField.textProperty().addListener(this::passwordsChanged);
|
|
||||||
retypePasswordField.textProperty().addListener(this::passwordsChanged);
|
|
||||||
|
|
||||||
passwordStrengthLevel0.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(0), strengthRater::getBackgroundWithStrengthColor));
|
|
||||||
passwordStrengthLevel1.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(1), strengthRater::getBackgroundWithStrengthColor));
|
|
||||||
passwordStrengthLevel2.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(2), strengthRater::getBackgroundWithStrengthColor));
|
|
||||||
passwordStrengthLevel3.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(3), strengthRater::getBackgroundWithStrengthColor));
|
|
||||||
passwordStrengthLevel4.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(4), strengthRater::getBackgroundWithStrengthColor));
|
|
||||||
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void passwordsChanged(@SuppressWarnings("unused") Observable observable) {
|
|
||||||
boolean oldPasswordEmpty = oldPasswordField.getCharacters().length() == 0;
|
|
||||||
boolean newPasswordEmpty = newPasswordField.getCharacters().length() == 0;
|
|
||||||
boolean passwordsEqual = newPasswordField.getCharacters().equals(retypePasswordField.getCharacters());
|
|
||||||
changePasswordButton.setDisable(oldPasswordEmpty || newPasswordEmpty || !passwordsEqual);
|
|
||||||
passwordStrength.set(strengthRater.computeRate(newPasswordField.getCharacters().toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Parent getRoot() {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void focus() {
|
|
||||||
oldPasswordField.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setVault(Vault vault) {
|
|
||||||
this.vault = Objects.requireNonNull(vault);
|
|
||||||
// trigger "default" change to refresh key bindings:
|
|
||||||
changePasswordButton.setDefaultButton(false);
|
|
||||||
changePasswordButton.setDefaultButton(true);
|
|
||||||
messageText.setText(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ****************************************
|
|
||||||
// Downloads link
|
|
||||||
// ****************************************
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
public void didClickDownloadsLink(ActionEvent event) {
|
|
||||||
app.getHostServices().showDocument("https://cryptomator.org/downloads/");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ****************************************
|
|
||||||
// Change password button
|
|
||||||
// ****************************************
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void didClickChangePasswordButton(ActionEvent event) {
|
|
||||||
downloadsPageLink.setVisible(false);
|
|
||||||
try {
|
|
||||||
vault.changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters());
|
|
||||||
messageText.setText(null);
|
|
||||||
listener.ifPresent(this::invokeListenerLater);
|
|
||||||
} catch (InvalidPassphraseException e) {
|
|
||||||
messageText.setText(localization.getString("changePassword.errorMessage.wrongPassword"));
|
|
||||||
Platform.runLater(oldPasswordField::requestFocus);
|
|
||||||
} catch (UncheckedIOException | IOException ex) {
|
|
||||||
messageText.setText(localization.getString("changePassword.errorMessage.decryptionFailed"));
|
|
||||||
LOG.error("Decryption failed for technical reasons.", ex);
|
|
||||||
} catch (UnsupportedVaultFormatException e) {
|
|
||||||
downloadsPageLink.setVisible(true);
|
|
||||||
LOG.warn("Unable to unlock vault: " + e.getMessage());
|
|
||||||
if (e.isVaultOlderThanSoftware()) {
|
|
||||||
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
|
|
||||||
} else if (e.isSoftwareOlderThanVault()) {
|
|
||||||
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
oldPasswordField.swipe();
|
|
||||||
newPasswordField.swipe();
|
|
||||||
retypePasswordField.swipe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Getter/Setter */
|
|
||||||
|
|
||||||
public ChangePasswordListener getListener() {
|
|
||||||
return listener.orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setListener(ChangePasswordListener listener) {
|
|
||||||
this.listener = Optional.ofNullable(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* callback */
|
|
||||||
|
|
||||||
private void invokeListenerLater(ChangePasswordListener listener) {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
listener.didChangePassword();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@FunctionalInterface
|
|
||||||
interface ChangePasswordListener {
|
|
||||||
void didChangePassword();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* Copyright (c) 2014, 2017 Sebastian Stenzel
|
|
||||||
* 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
|
|
||||||
* Jean-Noël Charon - password strength meter
|
|
||||||
******************************************************************************/
|
|
||||||
package org.cryptomator.ui.controllers;
|
|
||||||
|
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.beans.Observable;
|
|
||||||
import javafx.beans.property.IntegerProperty;
|
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.fxml.FXML;
|
|
||||||
import javafx.scene.Parent;
|
|
||||||
import javafx.scene.control.Button;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.layout.GridPane;
|
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
import org.cryptomator.ui.controls.SecPasswordField;
|
|
||||||
import org.cryptomator.ui.l10n.Localization;
|
|
||||||
import org.cryptomator.ui.model.Vault;
|
|
||||||
import org.cryptomator.ui.util.PasswordStrengthUtil;
|
|
||||||
import org.fxmisc.easybind.EasyBind;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.FileAlreadyExistsException;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class InitializeController implements ViewController {
|
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
|
|
||||||
|
|
||||||
private final Localization localization;
|
|
||||||
private final PasswordStrengthUtil strengthRater;
|
|
||||||
private IntegerProperty passwordStrength = new SimpleIntegerProperty(-1); // strengths: 0-4
|
|
||||||
private Optional<InitializationListener> listener = Optional.empty();
|
|
||||||
private Vault vault;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public InitializeController(Localization localization, PasswordStrengthUtil strengthRater) {
|
|
||||||
this.localization = localization;
|
|
||||||
this.strengthRater = strengthRater;
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private SecPasswordField passwordField;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private SecPasswordField retypePasswordField;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Button okButton;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label messageLabel;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label passwordStrengthLabel;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Region passwordStrengthLevel0;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Region passwordStrengthLevel1;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Region passwordStrengthLevel2;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Region passwordStrengthLevel3;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Region passwordStrengthLevel4;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private GridPane root;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize() {
|
|
||||||
passwordField.textProperty().addListener(this::passwordsChanged);
|
|
||||||
retypePasswordField.textProperty().addListener(this::passwordsChanged);
|
|
||||||
|
|
||||||
passwordStrengthLevel0.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(0), strengthRater::getBackgroundWithStrengthColor));
|
|
||||||
passwordStrengthLevel1.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(1), strengthRater::getBackgroundWithStrengthColor));
|
|
||||||
passwordStrengthLevel2.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(2), strengthRater::getBackgroundWithStrengthColor));
|
|
||||||
passwordStrengthLevel3.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(3), strengthRater::getBackgroundWithStrengthColor));
|
|
||||||
passwordStrengthLevel4.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(4), strengthRater::getBackgroundWithStrengthColor));
|
|
||||||
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void passwordsChanged(@SuppressWarnings("unused") Observable observable) {
|
|
||||||
boolean passwordsEmpty = passwordField.getCharacters().length() == 0;
|
|
||||||
boolean passwordsEqual = passwordField.getCharacters().equals(retypePasswordField.getCharacters());
|
|
||||||
okButton.setDisable(passwordsEmpty || !passwordsEqual);
|
|
||||||
passwordStrength.set(strengthRater.computeRate(passwordField.getCharacters().toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Parent getRoot() {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void focus() {
|
|
||||||
passwordField.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setVault(Vault vault) {
|
|
||||||
this.vault = Objects.requireNonNull(vault);
|
|
||||||
// trigger "default" change to refresh key bindings:
|
|
||||||
okButton.setDefaultButton(false);
|
|
||||||
okButton.setDefaultButton(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ****************************************
|
|
||||||
// OK button
|
|
||||||
// ****************************************
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
protected void initializeVault(ActionEvent event) {
|
|
||||||
final CharSequence passphrase = passwordField.getCharacters();
|
|
||||||
try {
|
|
||||||
vault.create(passphrase);
|
|
||||||
listener.ifPresent(this::invokeListenerLater);
|
|
||||||
} catch (FileAlreadyExistsException ex) {
|
|
||||||
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
|
|
||||||
} catch (IOException ex) {
|
|
||||||
LOG.error("I/O Exception", ex);
|
|
||||||
messageLabel.setText(localization.getString("initialize.messageLabel.initializationFailed"));
|
|
||||||
} finally {
|
|
||||||
passwordField.swipe();
|
|
||||||
retypePasswordField.swipe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Getter/Setter */
|
|
||||||
|
|
||||||
public InitializationListener getListener() {
|
|
||||||
return listener.orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setListener(InitializationListener listener) {
|
|
||||||
this.listener = Optional.ofNullable(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* callback */
|
|
||||||
|
|
||||||
private void invokeListenerLater(InitializationListener listener) {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
listener.didInitialize();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@FunctionalInterface
|
|
||||||
interface InitializationListener {
|
|
||||||
void didInitialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,554 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* Copyright (c) 2014, 2017 Sebastian Stenzel
|
|
||||||
* 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
|
|
||||||
* Jean-Noël Charon - confirmation dialog on vault removal
|
|
||||||
******************************************************************************/
|
|
||||||
package org.cryptomator.ui.controllers;
|
|
||||||
|
|
||||||
import javafx.application.Application;
|
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.beans.binding.Binding;
|
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.binding.BooleanBinding;
|
|
||||||
import javafx.beans.binding.BooleanExpression;
|
|
||||||
import javafx.beans.binding.ObjectExpression;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import javafx.fxml.FXML;
|
|
||||||
import javafx.geometry.Side;
|
|
||||||
import javafx.scene.Parent;
|
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.scene.control.Alert;
|
|
||||||
import javafx.scene.control.Button;
|
|
||||||
import javafx.scene.control.ButtonType;
|
|
||||||
import javafx.scene.control.Cell;
|
|
||||||
import javafx.scene.control.ContextMenu;
|
|
||||||
import javafx.scene.control.ListCell;
|
|
||||||
import javafx.scene.control.ListView;
|
|
||||||
import javafx.scene.control.MenuItem;
|
|
||||||
import javafx.scene.control.ToggleButton;
|
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import javafx.scene.input.KeyCode;
|
|
||||||
import javafx.scene.input.KeyEvent;
|
|
||||||
import javafx.scene.input.MouseEvent;
|
|
||||||
import javafx.scene.layout.HBox;
|
|
||||||
import javafx.scene.layout.Pane;
|
|
||||||
import javafx.scene.text.Font;
|
|
||||||
import javafx.stage.FileChooser;
|
|
||||||
import javafx.stage.Stage;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.cryptomator.common.FxApplicationScoped;
|
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
|
||||||
import org.cryptomator.ui.ExitUtil;
|
|
||||||
import org.cryptomator.ui.controls.DirectoryListCell;
|
|
||||||
import org.cryptomator.ui.l10n.Localization;
|
|
||||||
import org.cryptomator.ui.model.AppLaunchEvent;
|
|
||||||
import org.cryptomator.ui.model.AutoUnlocker;
|
|
||||||
import org.cryptomator.ui.model.Vault;
|
|
||||||
import org.cryptomator.ui.model.VaultFactory;
|
|
||||||
import org.cryptomator.ui.model.VaultList;
|
|
||||||
import org.cryptomator.ui.model.upgrade.UpgradeStrategies;
|
|
||||||
import org.cryptomator.ui.model.upgrade.UpgradeStrategy;
|
|
||||||
import org.cryptomator.ui.util.DialogBuilderUtil;
|
|
||||||
import org.cryptomator.ui.util.Tasks;
|
|
||||||
import org.fxmisc.easybind.EasyBind;
|
|
||||||
import org.fxmisc.easybind.Subscription;
|
|
||||||
import org.fxmisc.easybind.monadic.MonadicBinding;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import java.awt.Desktop;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.BlockingQueue;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static org.cryptomator.ui.util.DialogBuilderUtil.buildErrorDialog;
|
|
||||||
|
|
||||||
@FxApplicationScoped
|
|
||||||
public class MainController implements ViewController {
|
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
|
|
||||||
private static final String ACTIVE_WINDOW_STYLE_CLASS = "active-window";
|
|
||||||
private static final String INACTIVE_WINDOW_STYLE_CLASS = "inactive-window";
|
|
||||||
|
|
||||||
private final Stage mainWindow;
|
|
||||||
private final ExitUtil exitUtil;
|
|
||||||
private final Localization localization;
|
|
||||||
private final ExecutorService executorService;
|
|
||||||
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
|
|
||||||
private final VaultFactory vaultFactoy;
|
|
||||||
private final ViewControllerLoader viewControllerLoader;
|
|
||||||
private final ObjectProperty<ViewController> activeController = new SimpleObjectProperty<>();
|
|
||||||
private final ObservableList<Vault> vaults;
|
|
||||||
private final BooleanBinding areAllVaultsLocked;
|
|
||||||
private final ObjectProperty<Vault> selectedVault = new SimpleObjectProperty<>();
|
|
||||||
private final ObjectExpression<Vault.State> selectedVaultState = ObjectExpression.objectExpression(EasyBind.select(selectedVault).selectObject(Vault::stateProperty));
|
|
||||||
private final BooleanExpression isSelectedVaultValid = BooleanExpression.booleanExpression(EasyBind.monadic(selectedVault).map(Vault::isValidVaultDirectory).orElse(false));
|
|
||||||
private final BooleanExpression canEditSelectedVault = selectedVaultState.isEqualTo(Vault.State.LOCKED);
|
|
||||||
private final MonadicBinding<UpgradeStrategy> upgradeStrategyForSelectedVault;
|
|
||||||
private final BooleanBinding isShowingSettings;
|
|
||||||
private final Map<Vault, UnlockedController> unlockedVaults = new HashMap<>();
|
|
||||||
|
|
||||||
private Subscription subs = Subscription.EMPTY;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public MainController(@Named("mainWindow") Stage mainWindow, ExecutorService executorService, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue, ExitUtil exitUtil, Localization localization,
|
|
||||||
VaultFactory vaultFactoy, ViewControllerLoader viewControllerLoader, UpgradeStrategies upgradeStrategies, VaultList vaults, AutoUnlocker autoUnlocker) {
|
|
||||||
this.mainWindow = mainWindow;
|
|
||||||
this.executorService = executorService;
|
|
||||||
this.launchEventQueue = launchEventQueue;
|
|
||||||
this.exitUtil = exitUtil;
|
|
||||||
this.localization = localization;
|
|
||||||
this.vaultFactoy = vaultFactoy;
|
|
||||||
this.viewControllerLoader = viewControllerLoader;
|
|
||||||
this.vaults = vaults;
|
|
||||||
|
|
||||||
// derived bindings:
|
|
||||||
this.isShowingSettings = Bindings.equal(SettingsController.class, EasyBind.monadic(activeController).map(ViewController::getClass));
|
|
||||||
this.upgradeStrategyForSelectedVault = EasyBind.monadic(selectedVault).map(upgradeStrategies::getUpgradeStrategy);
|
|
||||||
this.areAllVaultsLocked = Bindings.isEmpty(FXCollections.observableList(vaults, Vault::observables).filtered(Vault.NOT_LOCKED));
|
|
||||||
|
|
||||||
EasyBind.subscribe(areAllVaultsLocked, exitUtil::updateTrayIcon);
|
|
||||||
EasyBind.subscribe(areAllVaultsLocked, Platform::setImplicitExit);
|
|
||||||
autoUnlocker.unlockAllSilently();
|
|
||||||
|
|
||||||
try {
|
|
||||||
Desktop.getDesktop().setPreferencesHandler(e -> {
|
|
||||||
Platform.runLater(this::toggleShowSettings);
|
|
||||||
});
|
|
||||||
} catch (UnsupportedOperationException e) {
|
|
||||||
LOG.info("Unable to setPreferencesHandler, probably not supported on this OS.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ContextMenu vaultListCellContextMenu;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private MenuItem changePasswordMenuItem;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ContextMenu addVaultContextMenu;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private HBox root;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ListView<Vault> vaultList;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ToggleButton addVaultButton;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Button removeVaultButton;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ToggleButton settingsButton;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Pane contentPane;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Pane emptyListInstructions;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize() {
|
|
||||||
vaultList.setItems(vaults);
|
|
||||||
vaultList.getSelectionModel().clearSelection();
|
|
||||||
vaultList.setOnKeyReleased(this::didPressKeyOnList);
|
|
||||||
vaultList.setCellFactory(this::createDirecoryListCell);
|
|
||||||
root.setOnKeyReleased(this::didPressKeyOnRoot);
|
|
||||||
activeController.set(viewControllerLoader.load("/fxml/welcome.fxml"));
|
|
||||||
selectedVault.bind(vaultList.getSelectionModel().selectedItemProperty());
|
|
||||||
removeVaultButton.disableProperty().bind(canEditSelectedVault.not());
|
|
||||||
emptyListInstructions.visibleProperty().bind(Bindings.isEmpty(vaults));
|
|
||||||
changePasswordMenuItem.visibleProperty().bind(isSelectedVaultValid.and(Bindings.isNull(upgradeStrategyForSelectedVault)));
|
|
||||||
|
|
||||||
subs = subs.and(EasyBind.subscribe(selectedVault, this::selectedVaultDidChange));
|
|
||||||
subs = subs.and(EasyBind.subscribe(activeController, this::activeControllerDidChange));
|
|
||||||
subs = subs.and(EasyBind.subscribe(isShowingSettings, settingsButton::setSelected));
|
|
||||||
subs = subs.and(EasyBind.subscribe(addVaultContextMenu.showingProperty(), addVaultButton::setSelected));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Parent getRoot() {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initStage(Stage stage) {
|
|
||||||
stage.setScene(new Scene(getRoot()));
|
|
||||||
stage.sizeToScene();
|
|
||||||
stage.setTitle(localization.getString("app.name")); // set once before bind to avoid display bugs with Linux window managers
|
|
||||||
stage.titleProperty().bind(windowTitle());
|
|
||||||
stage.setResizable(false);
|
|
||||||
loadFont("/css/ionicons.ttf");
|
|
||||||
loadFont("/css/fontawesome-webfont.ttf");
|
|
||||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
|
||||||
subs = subs.and(EasyBind.includeWhen(mainWindow.getScene().getRoot().getStyleClass(), ACTIVE_WINDOW_STYLE_CLASS, mainWindow.focusedProperty()));
|
|
||||||
subs = subs.and(EasyBind.includeWhen(mainWindow.getScene().getRoot().getStyleClass(), INACTIVE_WINDOW_STYLE_CLASS, mainWindow.focusedProperty().not()));
|
|
||||||
Application.setUserAgentStylesheet(getClass().getResource("/css/mac_theme.css").toString());
|
|
||||||
} else if (SystemUtils.IS_OS_LINUX) {
|
|
||||||
stage.getIcons().add(new Image(getClass().getResourceAsStream("/window_icon_512.png")));
|
|
||||||
Application.setUserAgentStylesheet(getClass().getResource("/css/linux_theme.css").toString());
|
|
||||||
} else if (SystemUtils.IS_OS_WINDOWS) {
|
|
||||||
stage.getIcons().add(new Image(getClass().getResourceAsStream("/window_icon_32.png")));
|
|
||||||
Application.setUserAgentStylesheet(getClass().getResource("/css/win_theme.css").toString());
|
|
||||||
}
|
|
||||||
exitUtil.initExitHandler(() -> Platform.runLater(this::gracefulShutdown));
|
|
||||||
listenToFileOpenRequests(stage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void gracefulShutdown() {
|
|
||||||
vaults.filtered(Vault.NOT_LOCKED).forEach(Vault::prepareForShutdown);
|
|
||||||
if (!vaults.filtered(Vault.NOT_LOCKED).isEmpty()) {
|
|
||||||
mainWindow.show(); // to keep the application open
|
|
||||||
ButtonType tryAgainButtonType = new ButtonType(localization.getString("main.gracefulShutdown.button.tryAgain"));
|
|
||||||
ButtonType forceShutdownButtonType = new ButtonType(localization.getString("main.gracefulShutdown.button.forceShutdown"));
|
|
||||||
Alert gracefulShutdownDialog = DialogBuilderUtil.buildGracefulShutdownDialog(
|
|
||||||
localization.getString("main.gracefulShutdown.dialog.title"), localization.getString("main.gracefulShutdown.dialog.header"), localization.getString("main.gracefulShutdown.dialog.content"),
|
|
||||||
forceShutdownButtonType, ButtonType.CANCEL, forceShutdownButtonType, tryAgainButtonType);
|
|
||||||
Optional<ButtonType> choice = gracefulShutdownDialog.showAndWait();
|
|
||||||
choice.ifPresent(btnType -> {
|
|
||||||
if (tryAgainButtonType.equals(btnType)) {
|
|
||||||
gracefulShutdown();
|
|
||||||
} else if (forceShutdownButtonType.equals(btnType)) {
|
|
||||||
Platform.runLater(Platform::exit);
|
|
||||||
} else {
|
|
||||||
if (!vaults.filtered(Vault.NOT_LOCKED).isEmpty()) {
|
|
||||||
showUnlockedView(vaults.get(0), false); //if there are still unlocked vaults, show one of them
|
|
||||||
} else {
|
|
||||||
showUnlockView(UnlockController.State.UNLOCKING); //otherwise show any vault
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Platform.runLater(Platform::exit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadFont(String resourcePath) {
|
|
||||||
try (InputStream in = getClass().getResourceAsStream(resourcePath)) {
|
|
||||||
Font.loadFont(in, 12.0);
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.warn("Error loading font from path: " + resourcePath, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void listenToFileOpenRequests(Stage stage) {
|
|
||||||
Tasks.create(launchEventQueue::take).onSuccess(event -> {
|
|
||||||
stage.setIconified(false);
|
|
||||||
stage.show();
|
|
||||||
stage.toFront();
|
|
||||||
stage.requestFocus();
|
|
||||||
event.getPathsToOpen().forEach(path -> addVault(path, true));
|
|
||||||
}).schedulePeriodically(executorService, Duration.ZERO, Duration.ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ListCell<Vault> createDirecoryListCell(ListView<Vault> param) {
|
|
||||||
final DirectoryListCell cell = new DirectoryListCell();
|
|
||||||
cell.setVaultContextMenu(vaultListCellContextMenu);
|
|
||||||
cell.setOnMouseClicked(this::didClickOnListCell);
|
|
||||||
return cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ****************************************
|
|
||||||
// UI Events
|
|
||||||
// ****************************************
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void didClickAddVault() {
|
|
||||||
if (addVaultContextMenu.isShowing()) {
|
|
||||||
addVaultContextMenu.hide();
|
|
||||||
} else {
|
|
||||||
addVaultContextMenu.show(addVaultButton, Side.BOTTOM, 0.0, 0.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void didClickCreateNewVault() {
|
|
||||||
final FileChooser fileChooser = new FileChooser();
|
|
||||||
final File file = fileChooser.showSaveDialog(mainWindow);
|
|
||||||
if (file == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
final Path vaultDir = file.toPath();
|
|
||||||
if (Files.exists(vaultDir)) {
|
|
||||||
try (Stream<Path> stream = Files.list(vaultDir)) {
|
|
||||||
if (stream.filter(this::isNotHidden).findAny().isPresent()) {
|
|
||||||
buildErrorDialog( //
|
|
||||||
localization.getString("main.createVault.nonEmptyDir.title"), //
|
|
||||||
localization.getString("main.createVault.nonEmptyDir.header"), //
|
|
||||||
localization.getString("main.createVault.nonEmptyDir.content"), //
|
|
||||||
ButtonType.OK).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Files.createDirectory(vaultDir);
|
|
||||||
}
|
|
||||||
addVault(vaultDir, true);
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.error("Unable to create vault", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isNotHidden(Path file) {
|
|
||||||
return !file.getFileName().toString().startsWith(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void didClickAddExistingVaults() {
|
|
||||||
final FileChooser fileChooser = new FileChooser();
|
|
||||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
|
|
||||||
final List<File> files = fileChooser.showOpenMultipleDialog(mainWindow);
|
|
||||||
if (files != null) {
|
|
||||||
for (final File file : files) {
|
|
||||||
addVault(file.toPath(), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* adds the given directory or selects it if it is already in the list of directories.
|
|
||||||
*
|
|
||||||
* @param path to a vault directory or masterkey file
|
|
||||||
*/
|
|
||||||
public void addVault(final Path path, boolean select) {
|
|
||||||
final Path vaultPath;
|
|
||||||
if (path != null && Files.isDirectory(path)) {
|
|
||||||
vaultPath = path;
|
|
||||||
} else if (path != null && Files.isReadable(path)) {
|
|
||||||
vaultPath = path.getParent();
|
|
||||||
} else {
|
|
||||||
LOG.warn("Ignoring attempt to add vault with invalid path: {}", path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Vault vault = vaults.stream().filter(v -> v.getPath().equals(vaultPath)).findAny().orElseGet(() -> {
|
|
||||||
VaultSettings vaultSettings = VaultSettings.withRandomId();
|
|
||||||
vaultSettings.path().set(vaultPath);
|
|
||||||
return vaultFactoy.get(vaultSettings);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!vaults.contains(vault)) {
|
|
||||||
vaults.add(vault);
|
|
||||||
}
|
|
||||||
if (select) {
|
|
||||||
vaultList.getSelectionModel().select(vault);
|
|
||||||
activeController.get().focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void didClickRemoveSelectedEntry() {
|
|
||||||
Alert confirmDialog = DialogBuilderUtil.buildConfirmationDialog( //
|
|
||||||
localization.getString("main.directoryList.remove.confirmation.title"), //
|
|
||||||
localization.getString("main.directoryList.remove.confirmation.header"), //
|
|
||||||
localization.getString("main.directoryList.remove.confirmation.content"), //
|
|
||||||
SystemUtils.IS_OS_MAC_OSX ? ButtonType.CANCEL : ButtonType.OK);
|
|
||||||
|
|
||||||
Optional<ButtonType> choice = confirmDialog.showAndWait();
|
|
||||||
if (ButtonType.OK.equals(choice.get())) {
|
|
||||||
vaults.remove(selectedVault.get());
|
|
||||||
if (vaults.isEmpty()) {
|
|
||||||
activeController.set(viewControllerLoader.load("/fxml/welcome.fxml"));
|
|
||||||
} else {
|
|
||||||
activeController.get().focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void didClickChangePassword() {
|
|
||||||
showChangePasswordView();
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void didClickShowSettings() {
|
|
||||||
toggleShowSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void toggleShowSettings() {
|
|
||||||
if (isShowingSettings.get()) {
|
|
||||||
showWelcomeView();
|
|
||||||
} else {
|
|
||||||
showPreferencesView();
|
|
||||||
}
|
|
||||||
vaultList.getSelectionModel().clearSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ****************************************
|
|
||||||
// Binding Listeners
|
|
||||||
// ****************************************
|
|
||||||
|
|
||||||
private void activeControllerDidChange(ViewController newValue) {
|
|
||||||
final Parent root = newValue.getRoot();
|
|
||||||
contentPane.getChildren().clear();
|
|
||||||
contentPane.getChildren().add(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void selectedVaultDidChange(Vault newValue) {
|
|
||||||
if (newValue == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (newValue.getState() != Vault.State.LOCKED) {
|
|
||||||
this.showUnlockedView(newValue, false);
|
|
||||||
} else if (!newValue.doesVaultDirectoryExist()) {
|
|
||||||
this.showNotFoundView();
|
|
||||||
} else if (newValue.isValidVaultDirectory() && upgradeStrategyForSelectedVault.isPresent()) {
|
|
||||||
this.showUpgradeView();
|
|
||||||
} else if (newValue.isValidVaultDirectory()) {
|
|
||||||
this.showUnlockView(UnlockController.State.UNLOCKING);
|
|
||||||
} else {
|
|
||||||
this.showInitializeView();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void didPressKeyOnList(KeyEvent e) {
|
|
||||||
if (e.getCode() == KeyCode.ENTER || e.getCode() == KeyCode.SPACE) {
|
|
||||||
activeController.get().focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void didPressKeyOnRoot(KeyEvent event) {
|
|
||||||
boolean triggered;
|
|
||||||
if (SystemUtils.IS_OS_MAC) {
|
|
||||||
triggered = event.isMetaDown();
|
|
||||||
} else {
|
|
||||||
triggered = event.isControlDown() && !event.isAltDown();
|
|
||||||
}
|
|
||||||
if (triggered && event.getCode().isDigitKey()) {
|
|
||||||
int digit = Integer.valueOf(event.getText());
|
|
||||||
switch (digit) {
|
|
||||||
case 0: {
|
|
||||||
vaultList.getSelectionModel().clearSelection();
|
|
||||||
showWelcomeView();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
vaultList.getSelectionModel().select(digit - 1);
|
|
||||||
activeController.get().focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void didClickOnListCell(MouseEvent e) {
|
|
||||||
if (MouseEvent.MOUSE_CLICKED.equals(e.getEventType()) && e.getSource() instanceof Cell && ((Cell<?>) e.getSource()).isSelected()) {
|
|
||||||
activeController.get().focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ****************************************
|
|
||||||
// Public Bindings
|
|
||||||
// ****************************************
|
|
||||||
|
|
||||||
public Binding<String> windowTitle() {
|
|
||||||
return EasyBind.monadic(selectedVault).flatMap(Vault::name).orElse(localization.getString("app.name"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ****************************************
|
|
||||||
// Subcontroller for right panel
|
|
||||||
// ****************************************
|
|
||||||
|
|
||||||
private void showWelcomeView() {
|
|
||||||
activeController.set(viewControllerLoader.load("/fxml/welcome.fxml"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showPreferencesView() {
|
|
||||||
activeController.set(viewControllerLoader.load("/fxml/settings.fxml"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showNotFoundView() {
|
|
||||||
activeController.set(viewControllerLoader.load("/fxml/notfound.fxml"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showInitializeView() {
|
|
||||||
final InitializeController ctrl = viewControllerLoader.load("/fxml/initialize.fxml");
|
|
||||||
ctrl.setVault(selectedVault.get());
|
|
||||||
ctrl.setListener(this::didInitialize);
|
|
||||||
activeController.set(ctrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void didInitialize() {
|
|
||||||
showUnlockView(UnlockController.State.INITIALIZED);
|
|
||||||
activeController.get().focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showUpgradeView() {
|
|
||||||
final UpgradeController ctrl = viewControllerLoader.load("/fxml/upgrade.fxml");
|
|
||||||
ctrl.setVault(selectedVault.get());
|
|
||||||
ctrl.setListener(this::didUpgrade);
|
|
||||||
activeController.set(ctrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void didUpgrade() {
|
|
||||||
showUnlockView(UnlockController.State.UPGRADED);
|
|
||||||
activeController.get().focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showUnlockView(UnlockController.State state) {
|
|
||||||
final UnlockController ctrl = viewControllerLoader.load("/fxml/unlock.fxml");
|
|
||||||
ctrl.setVault(selectedVault.get(), state);
|
|
||||||
ctrl.setListener(this::didUnlock);
|
|
||||||
activeController.set(ctrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void didUnlock(Vault vault) {
|
|
||||||
if (vault.equals(selectedVault.getValue())) {
|
|
||||||
this.showUnlockedView(vault, vault.getVaultSettings().revealAfterMount().getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showUnlockedView(Vault vault, boolean reveal) {
|
|
||||||
final UnlockedController ctrl = unlockedVaults.computeIfAbsent(vault, k -> viewControllerLoader.load("/fxml/unlocked.fxml"));
|
|
||||||
ctrl.setVault(vault);
|
|
||||||
ctrl.setListener(this::didLock);
|
|
||||||
if (reveal) {
|
|
||||||
ctrl.revealVault(vault);
|
|
||||||
}
|
|
||||||
activeController.set(ctrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void didLock(UnlockedController ctrl) {
|
|
||||||
unlockedVaults.remove(ctrl.getVault());
|
|
||||||
if (ctrl.getVault().getId() == selectedVault.get().getId()) {
|
|
||||||
showUnlockView(UnlockController.State.UNLOCKING);
|
|
||||||
}
|
|
||||||
activeController.get().focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showChangePasswordView() {
|
|
||||||
final ChangePasswordController ctrl = viewControllerLoader.load("/fxml/change_password.fxml");
|
|
||||||
ctrl.setVault(selectedVault.get());
|
|
||||||
ctrl.setListener(this::didChangePassword);
|
|
||||||
activeController.set(ctrl);
|
|
||||||
Platform.runLater(ctrl::focus);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void didChangePassword() {
|
|
||||||
showUnlockView(UnlockController.State.PASSWORD_CHANGED);
|
|
||||||
activeController.get().focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the accompanying LICENSE file.
|
|
||||||
*******************************************************************************/
|
|
||||||
package org.cryptomator.ui.controllers;
|
|
||||||
|
|
||||||
import javafx.fxml.FXML;
|
|
||||||
import javafx.scene.Parent;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import org.cryptomator.common.FxApplicationScoped;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
@FxApplicationScoped
|
|
||||||
public class NotFoundController implements ViewController {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public NotFoundController() {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
VBox root;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Parent getRoot() {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* Copyright (c) 2014, 2017 Sebastian Stenzel
|
|
||||||
* 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.ui.controllers;
|
|
||||||
|
|
||||||
import com.google.common.base.CharMatcher;
|
|
||||||
import com.google.common.base.Strings;
|
|
||||||
import javafx.beans.Observable;
|
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.fxml.FXML;
|
|
||||||
import javafx.scene.Group;
|
|
||||||
import javafx.scene.Parent;
|
|
||||||
import javafx.scene.control.Button;
|
|
||||||
import javafx.scene.control.CheckBox;
|
|
||||||
import javafx.scene.control.ChoiceBox;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.control.TextField;
|
|
||||||
import javafx.scene.input.KeyEvent;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import javafx.util.StringConverter;
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.cryptomator.common.FxApplicationScoped;
|
|
||||||
import org.cryptomator.common.settings.Settings;
|
|
||||||
import org.cryptomator.common.settings.VolumeImpl;
|
|
||||||
import org.cryptomator.ui.l10n.Localization;
|
|
||||||
import org.cryptomator.ui.model.Volume;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@FxApplicationScoped
|
|
||||||
public class SettingsController implements ViewController {
|
|
||||||
|
|
||||||
private static final CharMatcher DIGITS_MATCHER = CharMatcher.inRange('0', '9');
|
|
||||||
|
|
||||||
private final Localization localization;
|
|
||||||
private final Settings settings;
|
|
||||||
private final Optional<String> applicationVersion;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public SettingsController(Localization localization, Settings settings, @Named("applicationVersion") Optional<String> applicationVersion) {
|
|
||||||
this.localization = localization;
|
|
||||||
this.settings = settings;
|
|
||||||
this.applicationVersion = applicationVersion;
|
|
||||||
this.webdavSettings = new Group();
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private CheckBox checkForUpdatesCheckbox;
|
|
||||||
|
|
||||||
private Group webdavSettings;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label portFieldLabel;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TextField portField;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Button changePortButton;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label versionLabel;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label prefGvfsSchemeLabel;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ChoiceBox<String> prefGvfsScheme;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ChoiceBox<VolumeImpl> volume;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private CheckBox debugModeCheckbox;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private VBox root;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize() {
|
|
||||||
versionLabel.setText(String.format(localization.getString("settings.version.label"), applicationVersion.orElse("SNAPSHOT")));
|
|
||||||
checkForUpdatesCheckbox.setDisable(areUpdatesManagedExternally());
|
|
||||||
checkForUpdatesCheckbox.setSelected(settings.checkForUpdates().get() && !areUpdatesManagedExternally());
|
|
||||||
|
|
||||||
//NIOADAPTER
|
|
||||||
volume.getItems().addAll(Volume.getCurrentSupportedAdapters());
|
|
||||||
volume.setValue(settings.preferredVolumeImpl().get());
|
|
||||||
volume.setConverter(new NioAdapterImplStringConverter());
|
|
||||||
volume.valueProperty().addListener(this::setVisibilityGvfsElements);
|
|
||||||
|
|
||||||
//WEBDAV
|
|
||||||
webdavSettings.visibleProperty().bind(volume.valueProperty().isEqualTo(VolumeImpl.WEBDAV));
|
|
||||||
webdavSettings.managedProperty().bind(webdavSettings.visibleProperty());
|
|
||||||
prefGvfsScheme.managedProperty().bind(webdavSettings.visibleProperty());
|
|
||||||
prefGvfsSchemeLabel.managedProperty().bind(webdavSettings.visibleProperty());
|
|
||||||
portFieldLabel.managedProperty().bind(webdavSettings.visibleProperty());
|
|
||||||
portFieldLabel.visibleProperty().bind(webdavSettings.visibleProperty());
|
|
||||||
changePortButton.managedProperty().bind(webdavSettings.visibleProperty());
|
|
||||||
portField.managedProperty().bind(webdavSettings.visibleProperty());
|
|
||||||
portField.visibleProperty().bind(webdavSettings.visibleProperty());
|
|
||||||
portField.setText(String.valueOf(settings.port().intValue()));
|
|
||||||
portField.addEventFilter(KeyEvent.KEY_TYPED, this::filterNumericKeyEvents);
|
|
||||||
changePortButton.visibleProperty().bind(settings.port().asString().isNotEqualTo(portField.textProperty()));
|
|
||||||
changePortButton.disableProperty().bind(Bindings.createBooleanBinding(this::isPortValid, portField.textProperty()).not());
|
|
||||||
prefGvfsScheme.getItems().add("dav");
|
|
||||||
prefGvfsScheme.getItems().add("webdav");
|
|
||||||
prefGvfsScheme.setValue(settings.preferredGvfsScheme().get());
|
|
||||||
prefGvfsSchemeLabel.setVisible(SystemUtils.IS_OS_LINUX);
|
|
||||||
prefGvfsScheme.setVisible(SystemUtils.IS_OS_LINUX);
|
|
||||||
|
|
||||||
debugModeCheckbox.setSelected(settings.debugMode().get());
|
|
||||||
|
|
||||||
settings.checkForUpdates().bind(checkForUpdatesCheckbox.selectedProperty());
|
|
||||||
settings.preferredGvfsScheme().bind(prefGvfsScheme.valueProperty());
|
|
||||||
settings.preferredVolumeImpl().bind(volume.valueProperty());
|
|
||||||
settings.debugMode().bind(debugModeCheckbox.selectedProperty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Parent getRoot() {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void changePort() {
|
|
||||||
assert isPortValid() : "Button must be disabled, if port is invalid.";
|
|
||||||
try {
|
|
||||||
int port = Integer.parseInt(portField.getText());
|
|
||||||
settings.port().set(port);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
throw new IllegalStateException("Button must be disabled, if port is invalid.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isPortValid() {
|
|
||||||
try {
|
|
||||||
int port = Integer.parseInt(portField.getText());
|
|
||||||
return port == 0 // choose port automatically
|
|
||||||
|| port >= Settings.MIN_PORT && port <= Settings.MAX_PORT; // port within range
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void filterNumericKeyEvents(KeyEvent t) {
|
|
||||||
if (!Strings.isNullOrEmpty(t.getCharacter()) && !DIGITS_MATCHER.matchesAllOf(t.getCharacter())) {
|
|
||||||
t.consume();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setVisibilityGvfsElements(@SuppressWarnings("unused") Observable obs, @SuppressWarnings("unused")Object oldValue, Object newValue) {
|
|
||||||
prefGvfsSchemeLabel.setVisible(SystemUtils.IS_OS_LINUX && ((VolumeImpl) newValue).getDisplayName().equals("WebDAV"));
|
|
||||||
prefGvfsScheme.setVisible(SystemUtils.IS_OS_LINUX && ((VolumeImpl) newValue).getDisplayName().equals("WebDAV"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean areUpdatesManagedExternally() {
|
|
||||||
return Boolean.parseBoolean(System.getProperty("cryptomator.updatesManagedExternally", "false"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class NioAdapterImplStringConverter extends StringConverter<VolumeImpl> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString(VolumeImpl object) {
|
|
||||||
return object.getDisplayName();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public VolumeImpl fromString(String string) {
|
|
||||||
return VolumeImpl.forDisplayName(string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,581 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* Copyright (c) 2014, 2017 Sebastian Stenzel
|
|
||||||
* 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.ui.controllers;
|
|
||||||
|
|
||||||
import com.google.common.base.CharMatcher;
|
|
||||||
import com.google.common.base.Strings;
|
|
||||||
import javafx.application.Application;
|
|
||||||
import javafx.beans.property.BooleanProperty;
|
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
|
||||||
import javafx.beans.value.ChangeListener;
|
|
||||||
import javafx.beans.value.ObservableValue;
|
|
||||||
import javafx.fxml.FXML;
|
|
||||||
import javafx.scene.Parent;
|
|
||||||
import javafx.scene.control.Alert;
|
|
||||||
import javafx.scene.control.Button;
|
|
||||||
import javafx.scene.control.ButtonType;
|
|
||||||
import javafx.scene.control.CheckBox;
|
|
||||||
import javafx.scene.control.ChoiceBox;
|
|
||||||
import javafx.scene.control.ContentDisplay;
|
|
||||||
import javafx.scene.control.Hyperlink;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.control.TextField;
|
|
||||||
import javafx.scene.input.KeyEvent;
|
|
||||||
import javafx.scene.layout.HBox;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import javafx.scene.text.Text;
|
|
||||||
import javafx.stage.DirectoryChooser;
|
|
||||||
import javafx.stage.Stage;
|
|
||||||
import javafx.util.StringConverter;
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.cryptomator.common.settings.Settings;
|
|
||||||
import org.cryptomator.common.settings.VaultSettings;
|
|
||||||
import org.cryptomator.common.settings.VolumeImpl;
|
|
||||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
|
||||||
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
|
|
||||||
import org.cryptomator.keychain.KeychainAccess;
|
|
||||||
import org.cryptomator.keychain.KeychainAccessException;
|
|
||||||
import org.cryptomator.ui.controls.SecPasswordField;
|
|
||||||
import org.cryptomator.ui.l10n.Localization;
|
|
||||||
import org.cryptomator.ui.model.Vault;
|
|
||||||
import org.cryptomator.ui.model.WindowsDriveLetters;
|
|
||||||
import org.cryptomator.ui.util.DialogBuilderUtil;
|
|
||||||
import org.cryptomator.ui.util.Tasks;
|
|
||||||
import org.fxmisc.easybind.EasyBind;
|
|
||||||
import org.fxmisc.easybind.Subscription;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import java.io.File;
|
|
||||||
import java.nio.file.DirectoryNotEmptyException;
|
|
||||||
import java.nio.file.NotDirectoryException;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
|
|
||||||
public class UnlockController implements ViewController {
|
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
|
|
||||||
private static final CharMatcher ALPHA_NUMERIC_MATCHER = CharMatcher.inRange('a', 'z') //
|
|
||||||
.or(CharMatcher.inRange('A', 'Z')) //
|
|
||||||
.or(CharMatcher.inRange('0', '9')) //
|
|
||||||
.or(CharMatcher.is('_')) //
|
|
||||||
.precomputed();
|
|
||||||
|
|
||||||
private final Application app;
|
|
||||||
private final Stage mainWindow;
|
|
||||||
private final Localization localization;
|
|
||||||
private final WindowsDriveLetters driveLetters;
|
|
||||||
private final ChangeListener<Path> driveLetterChangeListener = this::winDriveLetterDidChange;
|
|
||||||
private final Optional<KeychainAccess> keychainAccess;
|
|
||||||
private final Settings settings;
|
|
||||||
private final ExecutorService executor;
|
|
||||||
private Vault vault;
|
|
||||||
private Optional<UnlockListener> listener = Optional.empty();
|
|
||||||
private Subscription vaultSubs = Subscription.EMPTY;
|
|
||||||
private BooleanProperty unlocking = new SimpleBooleanProperty();
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public UnlockController(Application app, @Named("mainWindow") Stage mainWindow, Localization localization, WindowsDriveLetters driveLetters, Optional<KeychainAccess> keychainAccess, Settings settings, ExecutorService executor) {
|
|
||||||
this.app = app;
|
|
||||||
this.mainWindow = mainWindow;
|
|
||||||
this.localization = localization;
|
|
||||||
this.driveLetters = driveLetters;
|
|
||||||
this.keychainAccess = keychainAccess;
|
|
||||||
this.settings = settings;
|
|
||||||
this.executor = executor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private SecPasswordField passwordField;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Button advancedOptionsButton;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Button unlockButton;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Text messageText;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private CheckBox savePassword;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TextField mountName;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private CheckBox useCustomMountFlags;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TextField mountFlags;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private CheckBox revealAfterMount;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private CheckBox useCustomWinDriveLetter;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ChoiceBox<Path> winDriveLetter;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private CheckBox useCustomMountPoint;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private HBox customMountPoint;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label customMountPointLabel;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Hyperlink downloadsPageLink;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private VBox advancedOptions;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private VBox root;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private CheckBox unlockAfterStartup;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private CheckBox useReadOnlyMode;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize() {
|
|
||||||
advancedOptions.managedProperty().bind(advancedOptions.visibleProperty());
|
|
||||||
advancedOptions.disableProperty().bind(unlocking);
|
|
||||||
unlockButton.disableProperty().bind(unlocking.or(passwordField.textProperty().isEmpty()));
|
|
||||||
mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents);
|
|
||||||
mountName.textProperty().addListener(this::mountNameDidChange);
|
|
||||||
useReadOnlyMode.selectedProperty().addListener(this::useReadOnlyDidChange);
|
|
||||||
useCustomMountFlags.selectedProperty().addListener(this::useCustomMountFlagsDidChange);
|
|
||||||
mountFlags.disableProperty().bind(useCustomMountFlags.selectedProperty().not());
|
|
||||||
mountFlags.textProperty().addListener(this::mountFlagsDidChange);
|
|
||||||
savePassword.setDisable(!keychainAccess.isPresent());
|
|
||||||
unlockAfterStartup.disableProperty().bind(savePassword.disabledProperty().or(savePassword.selectedProperty().not()));
|
|
||||||
downloadsPageLink.visibleProperty().bind(downloadsPageLink.managedProperty());
|
|
||||||
|
|
||||||
customMountPoint.visibleProperty().bind(useCustomMountPoint.selectedProperty());
|
|
||||||
customMountPoint.managedProperty().bind(useCustomMountPoint.selectedProperty());
|
|
||||||
winDriveLetter.setConverter(new WinDriveLetterLabelConverter());
|
|
||||||
winDriveLetter.disableProperty().bind(useCustomWinDriveLetter.selectedProperty().not());
|
|
||||||
|
|
||||||
if (!SystemUtils.IS_OS_WINDOWS) {
|
|
||||||
useCustomWinDriveLetter.setVisible(false);
|
|
||||||
useCustomWinDriveLetter.setManaged(false);
|
|
||||||
winDriveLetter.setVisible(false);
|
|
||||||
winDriveLetter.setManaged(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Parent getRoot() {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void focus() {
|
|
||||||
passwordField.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setVault(Vault vault, State state) {
|
|
||||||
vaultSubs.unsubscribe();
|
|
||||||
vaultSubs = Subscription.EMPTY;
|
|
||||||
|
|
||||||
// trigger "default" change to refresh key bindings:
|
|
||||||
unlockButton.setDefaultButton(false);
|
|
||||||
unlockButton.setDefaultButton(true);
|
|
||||||
if (Objects.equals(this.vault, Objects.requireNonNull(vault))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
assert vault != null;
|
|
||||||
this.vault = vault;
|
|
||||||
advancedOptions.setVisible(false);
|
|
||||||
advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show"));
|
|
||||||
unlockButton.setContentDisplay(ContentDisplay.TEXT_ONLY);
|
|
||||||
state.successMessage().map(localization::getString).ifPresent(messageText::setText);
|
|
||||||
downloadsPageLink.setManaged(false);
|
|
||||||
mountName.setText(vault.getMountName());
|
|
||||||
useCustomMountFlags.setSelected(vault.isHavingCustomMountFlags());
|
|
||||||
mountFlags.setText(vault.getMountFlags());
|
|
||||||
savePassword.setSelected(false);
|
|
||||||
// auto-fill pw from keychain:
|
|
||||||
if (keychainAccess.isPresent()) {
|
|
||||||
try {
|
|
||||||
char[] storedPw = keychainAccess.get().loadPassphrase(vault.getId());
|
|
||||||
if (storedPw != null) {
|
|
||||||
savePassword.setSelected(true);
|
|
||||||
passwordField.setPassword(storedPw);
|
|
||||||
passwordField.selectRange(storedPw.length, storedPw.length);
|
|
||||||
Arrays.fill(storedPw, ' ');
|
|
||||||
}
|
|
||||||
} catch (KeychainAccessException e) {
|
|
||||||
LOG.error("Failed to load stored password from system keychain.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
VaultSettings vaultSettings = vault.getVaultSettings();
|
|
||||||
unlockAfterStartup.setSelected(savePassword.isSelected() && vaultSettings.unlockAfterStartup().get());
|
|
||||||
revealAfterMount.setSelected(vaultSettings.revealAfterMount().get());
|
|
||||||
useReadOnlyMode.setSelected(vaultSettings.usesReadOnlyMode().get());
|
|
||||||
|
|
||||||
// WEBDAV-dependent controls:
|
|
||||||
if (VolumeImpl.WEBDAV.equals(settings.preferredVolumeImpl().get())) {
|
|
||||||
useCustomMountPoint.setVisible(false);
|
|
||||||
useCustomMountPoint.setManaged(false);
|
|
||||||
useCustomMountFlags.setVisible(false);
|
|
||||||
useCustomMountFlags.setManaged(false);
|
|
||||||
mountFlags.setVisible(false);
|
|
||||||
mountFlags.setManaged(false);
|
|
||||||
} else {
|
|
||||||
useCustomMountPoint.setVisible(true);
|
|
||||||
useCustomMountPoint.setSelected(vaultSettings.usesIndividualMountPath().get());
|
|
||||||
if (Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) {
|
|
||||||
customMountPointLabel.setText(localization.getString("unlock.label.chooseMountPath"));
|
|
||||||
} else {
|
|
||||||
customMountPointLabel.setText(displayablePath(vaultSettings.individualMountPath().getValueSafe()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// OS-dependent controls:
|
|
||||||
if (SystemUtils.IS_OS_WINDOWS) {
|
|
||||||
winDriveLetter.valueProperty().removeListener(driveLetterChangeListener);
|
|
||||||
winDriveLetter.getItems().clear();
|
|
||||||
winDriveLetter.getItems().add(null);
|
|
||||||
winDriveLetter.getItems().addAll(driveLetters.getAvailableDriveLetters());
|
|
||||||
winDriveLetter.getItems().sort(new WinDriveLetterComparator());
|
|
||||||
winDriveLetter.valueProperty().addListener(driveLetterChangeListener);
|
|
||||||
chooseSelectedDriveLetter();
|
|
||||||
|
|
||||||
winDriveLetter.visibleProperty().bind(useCustomMountPoint.selectedProperty().not());
|
|
||||||
winDriveLetter.managedProperty().bind(useCustomMountPoint.selectedProperty().not());
|
|
||||||
useCustomWinDriveLetter.setSelected(!vaultSettings.usesIndividualMountPath().get());
|
|
||||||
useCustomWinDriveLetter.visibleProperty().bind(useCustomMountPoint.selectedProperty().not());
|
|
||||||
useCustomWinDriveLetter.managedProperty().bind(useCustomMountPoint.selectedProperty().not());
|
|
||||||
}
|
|
||||||
|
|
||||||
vaultSubs = vaultSubs.and(EasyBind.subscribe(unlockAfterStartup.selectedProperty(), vaultSettings.unlockAfterStartup()::set));
|
|
||||||
vaultSubs = vaultSubs.and(EasyBind.subscribe(revealAfterMount.selectedProperty(), vaultSettings.revealAfterMount()::set));
|
|
||||||
vaultSubs = vaultSubs.and(EasyBind.subscribe(useCustomMountPoint.selectedProperty(), vaultSettings.usesIndividualMountPath()::set));
|
|
||||||
vaultSubs = vaultSubs.and(EasyBind.subscribe(useReadOnlyMode.selectedProperty(), vaultSettings.usesReadOnlyMode()::set));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String displayablePath(String path) {
|
|
||||||
Path homeDir = Paths.get(SystemUtils.USER_HOME);
|
|
||||||
Path p = Paths.get(path);
|
|
||||||
if (p.startsWith(homeDir)) {
|
|
||||||
Path relativePath = homeDir.relativize(p);
|
|
||||||
String homePrefix = SystemUtils.IS_OS_WINDOWS ? "~\\" : "~/";
|
|
||||||
return homePrefix + relativePath.toString();
|
|
||||||
} else {
|
|
||||||
return p.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ****************************************
|
|
||||||
// Downloads link
|
|
||||||
// ****************************************
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
public void didClickDownloadsLink() {
|
|
||||||
app.getHostServices().showDocument("https://cryptomator.org/downloads/#allVersions");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ****************************************
|
|
||||||
// Advanced options button
|
|
||||||
// ****************************************
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void didClickAdvancedOptionsButton() {
|
|
||||||
messageText.setText(null);
|
|
||||||
downloadsPageLink.setManaged(false);
|
|
||||||
advancedOptions.setVisible(!advancedOptions.isVisible());
|
|
||||||
if (advancedOptions.isVisible()) {
|
|
||||||
advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.hide"));
|
|
||||||
} else {
|
|
||||||
advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void filterAlphanumericKeyEvents(KeyEvent t) {
|
|
||||||
if (!Strings.isNullOrEmpty(t.getCharacter()) && !ALPHA_NUMERIC_MATCHER.matchesAllOf(t.getCharacter())) {
|
|
||||||
t.consume();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void mountNameDidChange(@SuppressWarnings("unused") ObservableValue<? extends String> property, @SuppressWarnings("unused") String oldValue, String newValue) {
|
|
||||||
// newValue is guaranteed to be a-z0-9_, see #filterAlphanumericKeyEvents
|
|
||||||
if (newValue.isEmpty()) {
|
|
||||||
mountName.setText(vault.getMountName());
|
|
||||||
} else {
|
|
||||||
vault.setMountName(newValue);
|
|
||||||
}
|
|
||||||
if (!useCustomMountFlags.isSelected()) {
|
|
||||||
mountFlags.setText(vault.getMountFlags()); // update default flags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void useReadOnlyDidChange(@SuppressWarnings("unused") ObservableValue<? extends Boolean> property, @SuppressWarnings("unused") Boolean oldValue, Boolean newValue) {
|
|
||||||
vault.getVaultSettings().usesReadOnlyMode().setValue(newValue);
|
|
||||||
if (!useCustomMountFlags.isSelected()) {
|
|
||||||
mountFlags.setText(vault.getMountFlags()); // update default flags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void useCustomMountFlagsDidChange(@SuppressWarnings("unused") ObservableValue<? extends Boolean> property, @SuppressWarnings("unused") Boolean oldValue, Boolean newValue) {
|
|
||||||
if (!newValue) {
|
|
||||||
vault.setMountFlags(VaultSettings.DEFAULT_MOUNT_FLAGS);
|
|
||||||
mountFlags.setText(vault.getMountFlags());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void mountFlagsDidChange(@SuppressWarnings("unused") ObservableValue<? extends String> property, @SuppressWarnings("unused") String oldValue, String newValue) {
|
|
||||||
if (useCustomMountFlags.isSelected()) {
|
|
||||||
vault.setMountFlags(newValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
public void didClickChooseCustomMountPoint() {
|
|
||||||
DirectoryChooser dirChooser = new DirectoryChooser();
|
|
||||||
File file = dirChooser.showDialog(mainWindow);
|
|
||||||
if (file != null) {
|
|
||||||
customMountPointLabel.setText(displayablePath(file.toString()));
|
|
||||||
vault.setCustomMountPath(file.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
public void didClickCustomWinDriveLetterCheckbox() {
|
|
||||||
if (!useCustomWinDriveLetter.isSelected()) {
|
|
||||||
winDriveLetter.setValue(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
public void didClickCustomMountPointCheckbox() {
|
|
||||||
useCustomWinDriveLetter.setSelected(vault.getWinDriveLetter() != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts 'C' to "C:" to translate between model and GUI.
|
|
||||||
*/
|
|
||||||
private class WinDriveLetterLabelConverter extends StringConverter<Path> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString(Path root) {
|
|
||||||
if (root == null) {
|
|
||||||
return localization.getString("unlock.choicebox.winDriveLetter.auto");
|
|
||||||
} else if (root.endsWith("occupied")) {
|
|
||||||
return root.getRoot().toString().substring(0, 1) + " (" + localization.getString("unlock.choicebox.winDriveLetter.occupied") + ")";
|
|
||||||
} else {
|
|
||||||
return root.toString().substring(0, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Path fromString(String string) {
|
|
||||||
if (localization.getString("unlock.choicebox.winDriveLetter.auto").equals(string)) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return Path.of(string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Natural sorting of ASCII letters, but <code>null</code> always on first, as this is "auto-assign".
|
|
||||||
*/
|
|
||||||
private static class WinDriveLetterComparator implements Comparator<Path> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compare(Path c1, Path c2) {
|
|
||||||
if (c1 == null) {
|
|
||||||
return -1;
|
|
||||||
} else if (c2 == null) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return c1.compareTo(c2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void winDriveLetterDidChange(@SuppressWarnings("unused") ObservableValue<? extends Path> property, @SuppressWarnings("unused") Path oldValue, Path newValue) {
|
|
||||||
vault.setWinDriveLetter(newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void chooseSelectedDriveLetter() {
|
|
||||||
assert SystemUtils.IS_OS_WINDOWS;
|
|
||||||
// if the vault prefers a drive letter, that is currently occupied, this is our last chance to reset this:
|
|
||||||
if (vault.getWinDriveLetter() != null) {
|
|
||||||
final Path pickedRoot = Path.of(vault.getWinDriveLetter() + ":\\");
|
|
||||||
if (driveLetters.getOccupiedDriveLetters().contains(pickedRoot)) {
|
|
||||||
Path alteredPath = pickedRoot.resolve("occupied");
|
|
||||||
this.winDriveLetter.getItems().add(alteredPath);
|
|
||||||
this.winDriveLetter.getSelectionModel().select(alteredPath);
|
|
||||||
} else {
|
|
||||||
this.winDriveLetter.getSelectionModel().select(pickedRoot);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// first option is known to be 'auto-assign' due to #WinDriveLetterComparator.
|
|
||||||
this.winDriveLetter.getSelectionModel().selectFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ****************************************
|
|
||||||
// Save password checkbox
|
|
||||||
// ****************************************
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void didClickSavePasswordCheckbox() {
|
|
||||||
if (!savePassword.isSelected() && hasStoredPassword()) {
|
|
||||||
Alert confirmDialog = DialogBuilderUtil.buildConfirmationDialog( //
|
|
||||||
localization.getString("unlock.savePassword.delete.confirmation.title"), //
|
|
||||||
localization.getString("unlock.savePassword.delete.confirmation.header"), //
|
|
||||||
localization.getString("unlock.savePassword.delete.confirmation.content"), //
|
|
||||||
SystemUtils.IS_OS_MAC_OSX ? ButtonType.CANCEL : ButtonType.OK);
|
|
||||||
|
|
||||||
Optional<ButtonType> choice = confirmDialog.showAndWait();
|
|
||||||
if (ButtonType.OK.equals(choice.get())) {
|
|
||||||
try {
|
|
||||||
keychainAccess.get().deletePassphrase(vault.getId());
|
|
||||||
} catch (KeychainAccessException e) {
|
|
||||||
LOG.error("Failed to remove entry from system keychain.", e);
|
|
||||||
}
|
|
||||||
} else if (ButtonType.CANCEL.equals(choice.get())) {
|
|
||||||
savePassword.setSelected(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasStoredPassword() {
|
|
||||||
try {
|
|
||||||
char[] storedPw = keychainAccess.get().loadPassphrase(vault.getId());
|
|
||||||
boolean hasPw = (storedPw != null);
|
|
||||||
if (storedPw != null) {
|
|
||||||
Arrays.fill(storedPw, ' ');
|
|
||||||
}
|
|
||||||
return hasPw;
|
|
||||||
} catch (KeychainAccessException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ****************************************
|
|
||||||
// Unlock button
|
|
||||||
// ****************************************
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void didClickUnlockButton() {
|
|
||||||
unlocking.set(true);
|
|
||||||
advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show"));
|
|
||||||
unlockButton.setContentDisplay(ContentDisplay.LEFT);
|
|
||||||
|
|
||||||
CharSequence password = passwordField.getCharacters();
|
|
||||||
Tasks.create(() -> {
|
|
||||||
vault.unlock(password);
|
|
||||||
if (keychainAccess.isPresent() && savePassword.isSelected()) {
|
|
||||||
keychainAccess.get().storePassphrase(vault.getId(), password);
|
|
||||||
}
|
|
||||||
}).onSuccess(() -> {
|
|
||||||
messageText.setText(null);
|
|
||||||
downloadsPageLink.setManaged(false);
|
|
||||||
listener.ifPresent(lstnr -> lstnr.didUnlock(vault));
|
|
||||||
passwordField.swipe();
|
|
||||||
}).onError(InvalidPassphraseException.class, e -> {
|
|
||||||
messageText.setText(localization.getString("unlock.errorMessage.wrongPassword"));
|
|
||||||
downloadsPageLink.setManaged(false);
|
|
||||||
passwordField.selectAll();
|
|
||||||
passwordField.requestFocus();
|
|
||||||
}).onError(UnsupportedVaultFormatException.class, e -> {
|
|
||||||
if (e.isVaultOlderThanSoftware()) {
|
|
||||||
// whitespace after localized text used as separator between text and hyperlink
|
|
||||||
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
|
|
||||||
downloadsPageLink.setManaged(true);
|
|
||||||
} else if (e.isSoftwareOlderThanVault()) {
|
|
||||||
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
|
|
||||||
downloadsPageLink.setManaged(true);
|
|
||||||
} else if (e.getDetectedVersion() == Integer.MAX_VALUE) {
|
|
||||||
messageText.setText(localization.getString("unlock.errorMessage.unauthenticVersionMac"));
|
|
||||||
}
|
|
||||||
}).onError(NotDirectoryException.class, e -> {
|
|
||||||
LOG.error("Unlock failed. Mount point not a directory: {}", e.getMessage());
|
|
||||||
advancedOptions.setVisible(true);
|
|
||||||
messageText.setText(null);
|
|
||||||
downloadsPageLink.setManaged(false);
|
|
||||||
showUnlockFailedErrorDialog("unlock.failedDialog.content.mountPathNonExisting");
|
|
||||||
}).onError(DirectoryNotEmptyException.class, e -> {
|
|
||||||
LOG.error("Unlock failed. Mount point not empty: {}", e.getMessage());
|
|
||||||
advancedOptions.setVisible(true);
|
|
||||||
messageText.setText(null);
|
|
||||||
downloadsPageLink.setManaged(false);
|
|
||||||
showUnlockFailedErrorDialog("unlock.failedDialog.content.mountPathNotEmpty");
|
|
||||||
}).onError(Exception.class, e -> { // including RuntimeExceptions
|
|
||||||
LOG.error("Unlock failed for technical reasons.", e);
|
|
||||||
messageText.setText(localization.getString("unlock.errorMessage.unlockFailed"));
|
|
||||||
downloadsPageLink.setManaged(false);
|
|
||||||
}).andFinally(() -> {
|
|
||||||
unlocking.set(false);
|
|
||||||
unlockButton.setContentDisplay(ContentDisplay.TEXT_ONLY);
|
|
||||||
}).runOnce(executor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showUnlockFailedErrorDialog(String localizableContentKey) {
|
|
||||||
String title = localization.getString("unlock.failedDialog.title");
|
|
||||||
String header = localization.getString("unlock.failedDialog.header");
|
|
||||||
String content = localization.getString(localizableContentKey);
|
|
||||||
Alert alert = DialogBuilderUtil.buildErrorDialog(title, header, content, ButtonType.OK);
|
|
||||||
alert.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* callback */
|
|
||||||
|
|
||||||
public void setListener(UnlockListener listener) {
|
|
||||||
this.listener = Optional.ofNullable(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@FunctionalInterface
|
|
||||||
interface UnlockListener {
|
|
||||||
|
|
||||||
void didUnlock(Vault vault);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* state */
|
|
||||||
|
|
||||||
public enum State {
|
|
||||||
UNLOCKING(null), //
|
|
||||||
INITIALIZED("unlock.successLabel.vaultCreated"), //
|
|
||||||
PASSWORD_CHANGED("unlock.successLabel.passwordChanged"), //
|
|
||||||
UPGRADED("unlock.successLabel.upgraded");
|
|
||||||
|
|
||||||
private Optional<String> successMessage;
|
|
||||||
|
|
||||||
State(String successMessage) {
|
|
||||||
this.successMessage = Optional.ofNullable(successMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<String> successMessage() {
|
|
||||||
return successMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* Copyright (c) 2014, 2017 Sebastian Stenzel
|
|
||||||
* 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.ui.controllers;
|
|
||||||
|
|
||||||
import javafx.animation.Animation;
|
|
||||||
import javafx.animation.KeyFrame;
|
|
||||||
import javafx.animation.Timeline;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.event.EventHandler;
|
|
||||||
import javafx.fxml.FXML;
|
|
||||||
import javafx.geometry.Side;
|
|
||||||
import javafx.scene.Parent;
|
|
||||||
import javafx.scene.chart.LineChart;
|
|
||||||
import javafx.scene.chart.NumberAxis;
|
|
||||||
import javafx.scene.chart.XYChart.Data;
|
|
||||||
import javafx.scene.chart.XYChart.Series;
|
|
||||||
import javafx.scene.control.Alert;
|
|
||||||
import javafx.scene.control.ButtonType;
|
|
||||||
import javafx.scene.control.ContextMenu;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.control.ToggleButton;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import javafx.stage.PopupWindow.AnchorLocation;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import org.cryptomator.ui.l10n.Localization;
|
|
||||||
import org.cryptomator.ui.model.Vault;
|
|
||||||
import org.cryptomator.ui.util.DialogBuilderUtil;
|
|
||||||
import org.cryptomator.ui.util.Tasks;
|
|
||||||
import org.fxmisc.easybind.EasyBind;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
|
|
||||||
import static java.lang.String.format;
|
|
||||||
|
|
||||||
public class UnlockedController implements ViewController {
|
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(UnlockedController.class);
|
|
||||||
|
|
||||||
private static final int IO_SAMPLING_STEPS = 100;
|
|
||||||
private static final double IO_SAMPLING_INTERVAL = 0.5;
|
|
||||||
|
|
||||||
private final Localization localization;
|
|
||||||
private final ExecutorService executor;
|
|
||||||
private final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
|
|
||||||
private Optional<LockListener> listener = Optional.empty();
|
|
||||||
private Timeline ioAnimation;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label messageLabel;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private LineChart<Number, Number> ioGraph;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private NumberAxis xAxis;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ToggleButton moreOptionsButton;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ContextMenu moreOptionsMenu;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private VBox root;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public UnlockedController(Localization localization, ExecutorService executor) {
|
|
||||||
this.localization = localization;
|
|
||||||
this.executor = executor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize() {
|
|
||||||
EasyBind.subscribe(vault, this::vaultChanged);
|
|
||||||
EasyBind.subscribe(moreOptionsMenu.showingProperty(), moreOptionsButton::setSelected);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Parent getRoot() {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void vaultChanged(Vault newVault) {
|
|
||||||
if (newVault == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// (re)start throughput statistics:
|
|
||||||
stopIoSampling();
|
|
||||||
startIoSampling();
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void didClickLockVault() {
|
|
||||||
regularLockVault(this::lockVaultSucceeded);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void lockVaultSucceeded() {
|
|
||||||
listener.ifPresent(listener -> listener.didLock(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void regularLockVault(Runnable onSuccess) {
|
|
||||||
Tasks.create(() -> {
|
|
||||||
vault.get().lock(false);
|
|
||||||
}).onSuccess(() -> {
|
|
||||||
LOG.trace("Regular unmount succeeded.");
|
|
||||||
onSuccess.run();
|
|
||||||
}).onError(Exception.class, e -> {
|
|
||||||
onRegularUnmountVaultFailed(e, onSuccess);
|
|
||||||
}).runOnce(executor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onRegularUnmountVaultFailed(Exception e, Runnable onSuccess) {
|
|
||||||
if (vault.get().supportsForcedUnmount()) {
|
|
||||||
LOG.trace("Regular unmount failed.", e);
|
|
||||||
Alert confirmDialog = DialogBuilderUtil.buildYesNoDialog( //
|
|
||||||
format(localization.getString("unlocked.lock.force.confirmation.title"), vault.get().name().getValue()), //
|
|
||||||
localization.getString("unlocked.lock.force.confirmation.header"), //
|
|
||||||
localization.getString("unlocked.lock.force.confirmation.content"), //
|
|
||||||
ButtonType.NO);
|
|
||||||
|
|
||||||
Optional<ButtonType> choice = confirmDialog.showAndWait();
|
|
||||||
if (ButtonType.YES.equals(choice.get())) {
|
|
||||||
forcedLockVault(onSuccess);
|
|
||||||
} else {
|
|
||||||
LOG.trace("Unmount cancelled.", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG.error("Regular unmount failed.", e);
|
|
||||||
messageLabel.setText(localization.getString("unlocked.label.unmountFailed"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void forcedLockVault(Runnable onSuccess) {
|
|
||||||
Tasks.create(() -> {
|
|
||||||
vault.get().lock(true);
|
|
||||||
}).onSuccess(() -> {
|
|
||||||
LOG.trace("Forced unmount succeeded.");
|
|
||||||
onSuccess.run();
|
|
||||||
}).onError(Exception.class, e -> {
|
|
||||||
LOG.error("Forced unmount failed.", e);
|
|
||||||
messageLabel.setText(localization.getString("unlocked.label.unmountFailed"));
|
|
||||||
}).runOnce(executor);
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void didClickMoreOptions() {
|
|
||||||
if (moreOptionsMenu.isShowing()) {
|
|
||||||
moreOptionsMenu.hide();
|
|
||||||
} else {
|
|
||||||
moreOptionsMenu.setAnchorLocation(AnchorLocation.CONTENT_TOP_RIGHT);
|
|
||||||
moreOptionsMenu.show(moreOptionsButton, Side.BOTTOM, moreOptionsButton.getWidth(), 0.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void didClickRevealVault() {
|
|
||||||
revealVault(vault.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
void revealVault(Vault vault) {
|
|
||||||
Tasks.create(() -> {
|
|
||||||
vault.reveal();
|
|
||||||
}).onSuccess(() -> {
|
|
||||||
LOG.trace("Reveal succeeded.");
|
|
||||||
messageLabel.setText(null);
|
|
||||||
}).onError(Exception.class, e -> {
|
|
||||||
LOG.error("Reveal failed.", e);
|
|
||||||
messageLabel.setText(localization.getString("unlocked.label.revealFailed"));
|
|
||||||
}).runOnce(executor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ****************************************
|
|
||||||
// IO Graph
|
|
||||||
// ****************************************
|
|
||||||
|
|
||||||
private void startIoSampling() {
|
|
||||||
final Series<Number, Number> decryptedBytes = new Series<>();
|
|
||||||
decryptedBytes.setName(localization.getString("unlocked.label.statsDecrypted"));
|
|
||||||
final Series<Number, Number> encryptedBytes = new Series<>();
|
|
||||||
encryptedBytes.setName(localization.getString("unlocked.label.statsEncrypted"));
|
|
||||||
|
|
||||||
ioGraph.getData().add(decryptedBytes);
|
|
||||||
ioGraph.getData().add(encryptedBytes);
|
|
||||||
|
|
||||||
ioAnimation = new Timeline();
|
|
||||||
ioAnimation.getKeyFrames().add(new KeyFrame(Duration.seconds(IO_SAMPLING_INTERVAL), new IoSamplingAnimationHandler(decryptedBytes, encryptedBytes)));
|
|
||||||
ioAnimation.setCycleCount(Animation.INDEFINITE);
|
|
||||||
ioAnimation.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopIoSampling() {
|
|
||||||
if (ioAnimation != null) {
|
|
||||||
ioGraph.getData().clear();
|
|
||||||
ioAnimation.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class IoSamplingAnimationHandler implements EventHandler<ActionEvent> {
|
|
||||||
|
|
||||||
private static final double BYTES_TO_MEGABYTES_FACTOR = 1.0 / IO_SAMPLING_INTERVAL / 1024.0 / 1024.0;
|
|
||||||
private final Series<Number, Number> decryptedBytes;
|
|
||||||
private final Series<Number, Number> encryptedBytes;
|
|
||||||
|
|
||||||
public IoSamplingAnimationHandler(Series<Number, Number> decryptedBytes, Series<Number, Number> encryptedBytes) {
|
|
||||||
this.decryptedBytes = decryptedBytes;
|
|
||||||
this.encryptedBytes = encryptedBytes;
|
|
||||||
|
|
||||||
// initialize data once and change value of datapoints later:
|
|
||||||
for (int i = 0; i < IO_SAMPLING_STEPS; i++) {
|
|
||||||
decryptedBytes.getData().add(new Data<>(i, 0));
|
|
||||||
encryptedBytes.getData().add(new Data<>(i, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
xAxis.setLowerBound(0);
|
|
||||||
xAxis.setUpperBound(IO_SAMPLING_STEPS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handle(ActionEvent event) {
|
|
||||||
// move all values one step:
|
|
||||||
for (int i = 0; i < IO_SAMPLING_STEPS - 1; i++) {
|
|
||||||
int j = i + 1;
|
|
||||||
Number tmp = decryptedBytes.getData().get(j).getYValue();
|
|
||||||
decryptedBytes.getData().get(i).setYValue(tmp);
|
|
||||||
|
|
||||||
tmp = encryptedBytes.getData().get(j).getYValue();
|
|
||||||
encryptedBytes.getData().get(i).setYValue(tmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add latest value:
|
|
||||||
final long decBytes = vault.get().pollBytesRead();
|
|
||||||
final double decMb = decBytes * BYTES_TO_MEGABYTES_FACTOR;
|
|
||||||
final long encBytes = vault.get().pollBytesWritten();
|
|
||||||
final double encMb = encBytes * BYTES_TO_MEGABYTES_FACTOR;
|
|
||||||
decryptedBytes.getData().get(IO_SAMPLING_STEPS - 1).setYValue(decMb);
|
|
||||||
encryptedBytes.getData().get(IO_SAMPLING_STEPS - 1).setYValue(encMb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Getter/Setter */
|
|
||||||
|
|
||||||
public Vault getVault() {
|
|
||||||
return this.vault.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVault(Vault vault) {
|
|
||||||
this.vault.set(vault);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* callback */
|
|
||||||
|
|
||||||
public void setListener(LockListener listener) {
|
|
||||||
this.listener = Optional.ofNullable(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@FunctionalInterface
|
|
||||||
interface LockListener {
|
|
||||||
|
|
||||||
void didLock(UnlockedController ctrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the accompanying LICENSE file.
|
|
||||||
*******************************************************************************/
|
|
||||||
package org.cryptomator.ui.controllers;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
|
|
||||||
import javafx.beans.binding.BooleanExpression;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.fxml.FXML;
|
|
||||||
import javafx.scene.Parent;
|
|
||||||
import javafx.scene.control.Button;
|
|
||||||
import javafx.scene.control.CheckBox;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.control.ProgressIndicator;
|
|
||||||
import javafx.scene.layout.GridPane;
|
|
||||||
import org.cryptomator.ui.controls.SecPasswordField;
|
|
||||||
import org.cryptomator.ui.model.upgrade.UpgradeStrategies;
|
|
||||||
import org.cryptomator.ui.model.upgrade.UpgradeStrategy;
|
|
||||||
import org.cryptomator.ui.model.upgrade.UpgradeStrategy.UpgradeFailedException;
|
|
||||||
import org.cryptomator.ui.model.Vault;
|
|
||||||
import org.cryptomator.ui.util.Tasks;
|
|
||||||
import org.fxmisc.easybind.EasyBind;
|
|
||||||
|
|
||||||
public class UpgradeController implements ViewController {
|
|
||||||
|
|
||||||
private final ObjectProperty<UpgradeStrategy> strategy = new SimpleObjectProperty<>();
|
|
||||||
private final UpgradeStrategies strategies;
|
|
||||||
private final ExecutorService executor;
|
|
||||||
private Optional<UpgradeListener> listener = Optional.empty();
|
|
||||||
private Vault vault;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public UpgradeController(UpgradeStrategies strategies, ExecutorService executor) {
|
|
||||||
this.strategies = strategies;
|
|
||||||
this.executor = executor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label upgradeTitleLabel;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label upgradeMsgLabel;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private SecPasswordField passwordField;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private CheckBox confirmationCheckbox;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Button upgradeButton;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ProgressIndicator progressIndicator;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label errorLabel;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private GridPane root;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize() {
|
|
||||||
upgradeTitleLabel.textProperty().bind(EasyBind.monadic(strategy).map(this::upgradeTitle).orElse(""));
|
|
||||||
upgradeMsgLabel.textProperty().bind(EasyBind.monadic(strategy).map(this::upgradeMessage).orElse(""));
|
|
||||||
|
|
||||||
BooleanExpression passwordProvided = passwordField.textProperty().isNotEmpty().and(passwordField.disabledProperty().not());
|
|
||||||
BooleanExpression syncFinished = confirmationCheckbox.selectedProperty();
|
|
||||||
upgradeButton.disableProperty().bind(passwordProvided.not().or(syncFinished.not()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Parent getRoot() {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void focus() {
|
|
||||||
passwordField.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setVault(Vault vault) {
|
|
||||||
this.vault = Objects.requireNonNull(vault);
|
|
||||||
errorLabel.setText(null);
|
|
||||||
strategy.set(strategies.getUpgradeStrategy(vault));
|
|
||||||
// trigger "default" change to refresh key bindings:
|
|
||||||
upgradeButton.setDefaultButton(false);
|
|
||||||
upgradeButton.setDefaultButton(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ****************************************
|
|
||||||
// Upgrade label
|
|
||||||
// ****************************************
|
|
||||||
|
|
||||||
private String upgradeTitle(UpgradeStrategy instruction) {
|
|
||||||
return instruction.getTitle(vault);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String upgradeMessage(UpgradeStrategy instruction) {
|
|
||||||
return instruction.getMessage(vault);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ****************************************
|
|
||||||
// Upgrade button
|
|
||||||
// ****************************************
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void didClickUpgradeButton(ActionEvent event) {
|
|
||||||
EasyBind.monadic(strategy).ifPresent(this::upgrade);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void upgrade(UpgradeStrategy instruction) {
|
|
||||||
passwordField.setDisable(true);
|
|
||||||
progressIndicator.setVisible(true);
|
|
||||||
Tasks //
|
|
||||||
.create(() -> {
|
|
||||||
if (!instruction.isApplicable(vault)) {
|
|
||||||
throw new IllegalStateException("No ugprade needed for " + vault.getPath());
|
|
||||||
}
|
|
||||||
instruction.upgrade(vault, passwordField.getCharacters());
|
|
||||||
}) //
|
|
||||||
.onSuccess(this::showNextUpgrade) //
|
|
||||||
.onError(UpgradeFailedException.class, e -> {
|
|
||||||
errorLabel.setText(e.getLocalizedMessage());
|
|
||||||
}) //
|
|
||||||
.andFinally(() -> {
|
|
||||||
progressIndicator.setVisible(false);
|
|
||||||
passwordField.setDisable(false);
|
|
||||||
passwordField.swipe();
|
|
||||||
}).runOnce(executor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showNextUpgrade() {
|
|
||||||
errorLabel.setText(null);
|
|
||||||
UpgradeStrategy nextStrategy = strategies.getUpgradeStrategy(vault);
|
|
||||||
if (nextStrategy != null) {
|
|
||||||
strategy.set(nextStrategy);
|
|
||||||
} else {
|
|
||||||
listener.ifPresent(UpgradeListener::didUpgrade);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* callback */
|
|
||||||
|
|
||||||
public void setListener(UpgradeListener listener) {
|
|
||||||
this.listener = Optional.ofNullable(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@FunctionalInterface
|
|
||||||
interface UpgradeListener {
|
|
||||||
void didUpgrade();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the accompanying LICENSE file.
|
|
||||||
*******************************************************************************/
|
|
||||||
package org.cryptomator.ui.controllers;
|
|
||||||
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.ResourceBundle;
|
|
||||||
|
|
||||||
import javafx.fxml.Initializable;
|
|
||||||
import javafx.scene.Parent;
|
|
||||||
|
|
||||||
public interface ViewController extends Initializable {
|
|
||||||
|
|
||||||
Parent getRoot();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
default void initialize(URL location, ResourceBundle resources) {
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
default void initialize() {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
default void focus() {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the accompanying LICENSE file.
|
|
||||||
*******************************************************************************/
|
|
||||||
package org.cryptomator.ui.controllers;
|
|
||||||
|
|
||||||
import javafx.fxml.FXMLLoader;
|
|
||||||
import org.cryptomator.common.FxApplicationScoped;
|
|
||||||
import org.cryptomator.ui.l10n.Localization;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Provider;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.UncheckedIOException;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@FxApplicationScoped
|
|
||||||
public class ViewControllerLoader {
|
|
||||||
|
|
||||||
private final Map<Class<? extends ViewController>, Provider<ViewController>> controllerProviders;
|
|
||||||
private final Localization localization;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public ViewControllerLoader(Map<Class<? extends ViewController>, Provider<ViewController>> controllerProviders, Localization localization) {
|
|
||||||
this.controllerProviders = controllerProviders;
|
|
||||||
this.localization = localization;
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T extends ViewController> T load(String resourceName) {
|
|
||||||
FXMLLoader loader = new FXMLLoader();
|
|
||||||
loader.setControllerFactory(this::constructController);
|
|
||||||
loader.setResources(localization);
|
|
||||||
try (InputStream in = getClass().getResourceAsStream(resourceName)) {
|
|
||||||
loader.load(in);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new UncheckedIOException("Error loading FXML: " + resourceName, e);
|
|
||||||
}
|
|
||||||
return loader.getController();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ViewController constructController(Class<?> clazz) {
|
|
||||||
Provider<ViewController> ctrlProvider = controllerProviders.get(clazz);
|
|
||||||
if (ctrlProvider == null) {
|
|
||||||
throw new IllegalStateException("No provider for type " + clazz.getName() + " registered.");
|
|
||||||
}
|
|
||||||
return ctrlProvider.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the accompanying LICENSE file.
|
|
||||||
*******************************************************************************/
|
|
||||||
package org.cryptomator.ui.controllers;
|
|
||||||
|
|
||||||
import dagger.Module;
|
|
||||||
import dagger.Provides;
|
|
||||||
import dagger.multibindings.IntoMap;
|
|
||||||
|
|
||||||
@Module
|
|
||||||
public class ViewControllerModule {
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@IntoMap
|
|
||||||
@ViewControllerKey(ChangePasswordController.class)
|
|
||||||
ViewController provideChangePasswordController(ChangePasswordController controller) {
|
|
||||||
return controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@IntoMap
|
|
||||||
@ViewControllerKey(InitializeController.class)
|
|
||||||
ViewController provideInitializeController(InitializeController controller) {
|
|
||||||
return controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@IntoMap
|
|
||||||
@ViewControllerKey(MainController.class)
|
|
||||||
ViewController provideMainController(MainController controller) {
|
|
||||||
return controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@IntoMap
|
|
||||||
@ViewControllerKey(NotFoundController.class)
|
|
||||||
ViewController provideNotFoundController(NotFoundController controller) {
|
|
||||||
return controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@IntoMap
|
|
||||||
@ViewControllerKey(SettingsController.class)
|
|
||||||
ViewController provideSettingsController(SettingsController controller) {
|
|
||||||
return controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@IntoMap
|
|
||||||
@ViewControllerKey(UnlockController.class)
|
|
||||||
ViewController provideUnlockController(UnlockController controller) {
|
|
||||||
return controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@IntoMap
|
|
||||||
@ViewControllerKey(UnlockedController.class)
|
|
||||||
ViewController provideUnlockedController(UnlockedController controller) {
|
|
||||||
return controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@IntoMap
|
|
||||||
@ViewControllerKey(UpgradeController.class)
|
|
||||||
ViewController provideUpgradeController(UpgradeController controller) {
|
|
||||||
return controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@IntoMap
|
|
||||||
@ViewControllerKey(WelcomeController.class)
|
|
||||||
ViewController provideWelcomeController(WelcomeController controller) {
|
|
||||||
return controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* Copyright (c) 2014, 2017 Sebastian Stenzel
|
|
||||||
* 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.ui.controllers;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
|
||||||
import javafx.application.Application;
|
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.fxml.FXML;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.Parent;
|
|
||||||
import javafx.scene.control.ButtonType;
|
|
||||||
import javafx.scene.control.Hyperlink;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.control.ProgressIndicator;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import org.apache.commons.lang3.SystemUtils;
|
|
||||||
import org.cryptomator.common.FxApplicationScoped;
|
|
||||||
import org.cryptomator.common.settings.Settings;
|
|
||||||
import org.cryptomator.ui.l10n.Localization;
|
|
||||||
import org.cryptomator.ui.util.Tasks;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.ResourceBundle;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static org.cryptomator.ui.util.DialogBuilderUtil.buildYesNoDialog;
|
|
||||||
|
|
||||||
@FxApplicationScoped
|
|
||||||
public class WelcomeController implements ViewController {
|
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(WelcomeController.class);
|
|
||||||
|
|
||||||
private final Application app;
|
|
||||||
private final Optional<String> applicationVersion;
|
|
||||||
private final Localization localization;
|
|
||||||
private final Settings settings;
|
|
||||||
private final Comparator<String> semVerComparator;
|
|
||||||
private final ScheduledExecutorService executor;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public WelcomeController(Application app, @Named("applicationVersion") Optional<String> applicationVersion, Localization localization, Settings settings, @Named("SemVer") Comparator<String> semVerComparator,
|
|
||||||
ScheduledExecutorService executor) {
|
|
||||||
this.app = app;
|
|
||||||
this.applicationVersion = applicationVersion;
|
|
||||||
this.localization = localization;
|
|
||||||
this.settings = settings;
|
|
||||||
this.semVerComparator = semVerComparator;
|
|
||||||
this.executor = executor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Node checkForUpdatesContainer;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label checkForUpdatesStatus;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ProgressIndicator checkForUpdatesIndicator;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Hyperlink updateLink;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private VBox root;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize(URL location, ResourceBundle resources) {
|
|
||||||
if (areUpdatesManagedExternally()) {
|
|
||||||
checkForUpdatesContainer.setVisible(false);
|
|
||||||
} else if (!settings.askedForUpdateCheck().get()) {
|
|
||||||
this.askForUpdateCheck();
|
|
||||||
} else if (settings.checkForUpdates().get()) {
|
|
||||||
this.checkForUpdates();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Parent getRoot() {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ****************************************
|
|
||||||
// Check for updates
|
|
||||||
// ****************************************
|
|
||||||
|
|
||||||
private boolean areUpdatesManagedExternally() {
|
|
||||||
return Boolean.parseBoolean(System.getProperty("cryptomator.updatesManagedExternally", "false"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void askForUpdateCheck() {
|
|
||||||
Tasks.create(() -> {}).onSuccess(() -> {
|
|
||||||
Optional<ButtonType> result = buildYesNoDialog(
|
|
||||||
localization.getString("welcome.askForUpdateCheck.dialog.title"),
|
|
||||||
localization.getString("welcome.askForUpdateCheck.dialog.header"),
|
|
||||||
localization.getString("welcome.askForUpdateCheck.dialog.content"),
|
|
||||||
ButtonType.YES).showAndWait();
|
|
||||||
if (result.isPresent()) {
|
|
||||||
settings.askedForUpdateCheck().set(true);
|
|
||||||
settings.checkForUpdates().set(result.get().equals(ButtonType.YES));
|
|
||||||
}
|
|
||||||
if (settings.checkForUpdates().get()) {
|
|
||||||
this.checkForUpdates();
|
|
||||||
}
|
|
||||||
}).scheduleOnce(executor, 1, TimeUnit.SECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkForUpdates() {
|
|
||||||
checkForUpdatesStatus.setText(localization.getString("welcome.checkForUpdates.label.currentlyChecking"));
|
|
||||||
checkForUpdatesIndicator.setVisible(true);
|
|
||||||
Tasks.create(() -> {
|
|
||||||
String userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", applicationVersion.orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
|
|
||||||
URL url = URI.create("https://api.cryptomator.org/updates/latestVersion.json").toURL();
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.addRequestProperty("User-Agent", userAgent);
|
|
||||||
conn.connect();
|
|
||||||
try {
|
|
||||||
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
|
|
||||||
return Optional.<byte[]>empty();
|
|
||||||
}
|
|
||||||
try (InputStream in = conn.getInputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
|
||||||
in.transferTo(out);
|
|
||||||
return Optional.of(out.toByteArray());
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
conn.disconnect();
|
|
||||||
}
|
|
||||||
}).onSuccess(response -> {
|
|
||||||
response.ifPresent(bytes -> {
|
|
||||||
Gson gson = new GsonBuilder().setLenient().create();
|
|
||||||
String json = new String(bytes, StandardCharsets.UTF_8);
|
|
||||||
Map<String, String> map = gson.fromJson(json, new TypeToken<Map<String, String>>() {
|
|
||||||
}.getType());
|
|
||||||
if (map != null) {
|
|
||||||
this.compareVersions(map);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).onError(Exception.class, e -> {
|
|
||||||
LOG.warn("Error checking for updates", e);
|
|
||||||
}).andFinally(() -> {
|
|
||||||
checkForUpdatesStatus.setText("");
|
|
||||||
checkForUpdatesIndicator.setVisible(false);
|
|
||||||
}).runOnce(executor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void compareVersions(final Map<String, String> latestVersions) {
|
|
||||||
assert Platform.isFxApplicationThread();
|
|
||||||
final String latestVersion;
|
|
||||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
|
||||||
latestVersion = latestVersions.get("mac");
|
|
||||||
} else if (SystemUtils.IS_OS_WINDOWS) {
|
|
||||||
latestVersion = latestVersions.get("win");
|
|
||||||
} else if (SystemUtils.IS_OS_LINUX) {
|
|
||||||
latestVersion = latestVersions.get("linux");
|
|
||||||
} else {
|
|
||||||
// no version check possible on unsupported OS
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final String currentVersion = applicationVersion.orElse(null);
|
|
||||||
LOG.info("Current version: {}, lastest version: {}", currentVersion, latestVersion);
|
|
||||||
if (currentVersion != null && semVerComparator.compare(currentVersion, latestVersion) < 0) {
|
|
||||||
final String msg = String.format(localization.getString("welcome.newVersionMessage"), latestVersion, currentVersion);
|
|
||||||
this.updateLink.setText(msg);
|
|
||||||
this.updateLink.setVisible(true);
|
|
||||||
this.updateLink.setDisable(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
public void didClickUpdateLink(ActionEvent event) {
|
|
||||||
app.getHostServices().showDocument("https://cryptomator.org/");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package org.cryptomator.ui.controls;
|
||||||
|
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.control.TextFormatter;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class AlphanumericTextField extends TextField {
|
||||||
|
|
||||||
|
private final static Pattern DIGIT_PATTERN = Pattern.compile("\\w*");
|
||||||
|
|
||||||
|
public AlphanumericTextField() {
|
||||||
|
this.setTextFormatter(new TextFormatter<>(this::filterNumericTextChange));
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextFormatter.Change filterNumericTextChange(TextFormatter.Change change) {
|
||||||
|
return DIGIT_PATTERN.matcher(change.getText()).matches() ? change : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,105 +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.ui.controls;
|
|
||||||
|
|
||||||
import org.cryptomator.ui.model.Vault;
|
|
||||||
import org.fxmisc.easybind.EasyBind;
|
|
||||||
|
|
||||||
import javafx.beans.binding.ObjectExpression;
|
|
||||||
import javafx.geometry.Pos;
|
|
||||||
import javafx.scene.control.ContentDisplay;
|
|
||||||
import javafx.scene.control.ContextMenu;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.control.OverrunStyle;
|
|
||||||
import javafx.scene.control.Tooltip;
|
|
||||||
import javafx.scene.layout.HBox;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.paint.Paint;
|
|
||||||
|
|
||||||
public class DirectoryListCell extends DraggableListCell<Vault> {
|
|
||||||
|
|
||||||
private static final Color UNLOCKED_ICON_COLOR = new Color(0.901, 0.494, 0.133, 1.0);
|
|
||||||
private final Label statusText = new Label();
|
|
||||||
private final Label nameText = new Label();
|
|
||||||
private final Label pathText = new Label();
|
|
||||||
private final VBox vbox = new VBox(4.0, nameText, pathText);
|
|
||||||
private final HBox hbox = new HBox(6.0, statusText, vbox);
|
|
||||||
|
|
||||||
private ContextMenu vaultContextMenu;
|
|
||||||
|
|
||||||
public DirectoryListCell() {
|
|
||||||
ObjectExpression<Vault.State> vaultState = ObjectExpression.objectExpression(EasyBind.select(itemProperty()).selectObject(Vault::stateProperty));
|
|
||||||
|
|
||||||
hbox.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
hbox.setPrefWidth(1);
|
|
||||||
vbox.setFillWidth(true);
|
|
||||||
|
|
||||||
nameText.textProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::name));
|
|
||||||
nameText.textFillProperty().bind(this.textFillProperty());
|
|
||||||
nameText.fontProperty().bind(this.fontProperty());
|
|
||||||
|
|
||||||
pathText.textProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::displayablePath));
|
|
||||||
pathText.setTextOverrun(OverrunStyle.ELLIPSIS);
|
|
||||||
pathText.getStyleClass().add("detail-label");
|
|
||||||
|
|
||||||
statusText.textProperty().bind(EasyBind.map(vaultState, this::getStatusIconText));
|
|
||||||
statusText.textFillProperty().bind(EasyBind.combine(vaultState, textFillProperty(), this::getStatusIconColor));
|
|
||||||
statusText.setMinSize(16.0, 16.0);
|
|
||||||
statusText.setAlignment(Pos.CENTER);
|
|
||||||
statusText.getStyleClass().add("fontawesome");
|
|
||||||
|
|
||||||
tooltipProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::displayablePath).map(p -> new Tooltip(p.toString())));
|
|
||||||
contextMenuProperty().bind(EasyBind.map(vaultState, this::getContextMenu));
|
|
||||||
|
|
||||||
setGraphic(hbox);
|
|
||||||
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getStatusIconText(Vault.State state) {
|
|
||||||
if (state == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
switch (state) {
|
|
||||||
case UNLOCKED:
|
|
||||||
case PROCESSING:
|
|
||||||
return "\uf09c";
|
|
||||||
case LOCKED:
|
|
||||||
default:
|
|
||||||
return "\uf023";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Paint getStatusIconColor(Vault.State state, Paint lockedValue) {
|
|
||||||
if (state == null) {
|
|
||||||
return lockedValue;
|
|
||||||
}
|
|
||||||
switch (state) {
|
|
||||||
case UNLOCKED:
|
|
||||||
case PROCESSING:
|
|
||||||
return UNLOCKED_ICON_COLOR;
|
|
||||||
case LOCKED:
|
|
||||||
default:
|
|
||||||
return lockedValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ContextMenu getContextMenu(Vault.State state) {
|
|
||||||
if (state == Vault.State.LOCKED) {
|
|
||||||
return vaultContextMenu;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVaultContextMenu(ContextMenu contextMenu) {
|
|
||||||
this.vaultContextMenu = contextMenu;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -8,11 +8,8 @@
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.ui.controls;
|
package org.cryptomator.ui.controls;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import java.util.Collections;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.scene.SnapshotParameters;
|
import javafx.scene.SnapshotParameters;
|
||||||
import javafx.scene.control.ListCell;
|
import javafx.scene.control.ListCell;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
@@ -21,22 +18,17 @@ import javafx.scene.input.DragEvent;
|
|||||||
import javafx.scene.input.Dragboard;
|
import javafx.scene.input.Dragboard;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
import javafx.scene.input.TransferMode;
|
import javafx.scene.input.TransferMode;
|
||||||
import javafx.scene.layout.Border;
|
import org.fxmisc.easybind.EasyBind;
|
||||||
import javafx.scene.layout.BorderImage;
|
|
||||||
import javafx.scene.layout.BorderStroke;
|
|
||||||
import javafx.scene.layout.BorderStrokeStyle;
|
|
||||||
import javafx.scene.layout.BorderWidths;
|
|
||||||
import javafx.scene.layout.CornerRadii;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.paint.Paint;
|
|
||||||
|
|
||||||
class DraggableListCell<T> extends ListCell<T> {
|
import java.util.List;
|
||||||
|
|
||||||
private static final double DROP_LINE_WIDTH = 4.0;
|
public class DraggableListCell<T> extends ListCell<T> {
|
||||||
private static final Paint DROP_LINE_COLOR = Color.gray(0.0, 0.6);
|
|
||||||
|
private static final String DROP_ABOVE_CLASS = "drop-above";
|
||||||
private final List<BorderStroke> defaultBorderStrokes;
|
private static final String DROP_BELOW_CLASS = "drop-below";
|
||||||
private final List<BorderImage> defaultBorderImages;
|
|
||||||
|
private final BooleanProperty dropAbove = new SimpleBooleanProperty();
|
||||||
|
private final BooleanProperty dropBelow = new SimpleBooleanProperty();
|
||||||
|
|
||||||
public DraggableListCell() {
|
public DraggableListCell() {
|
||||||
setOnDragDetected(this::onDragDetected);
|
setOnDragDetected(this::onDragDetected);
|
||||||
@@ -45,19 +37,25 @@ class DraggableListCell<T> extends ListCell<T> {
|
|||||||
setOnDragExited(this::onDragExited);
|
setOnDragExited(this::onDragExited);
|
||||||
setOnDragDropped(this::onDragDropped);
|
setOnDragDropped(this::onDragDropped);
|
||||||
setOnDragDone(DragEvent::consume);
|
setOnDragDone(DragEvent::consume);
|
||||||
this.defaultBorderStrokes = this.getBorder() == null ? Collections.emptyList() : this.getBorder().getStrokes();
|
|
||||||
this.defaultBorderImages = this.getBorder() == null ? Collections.emptyList() : this.getBorder().getImages();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Border createDropPositionBorder(double verticalCursorPosition) {
|
EasyBind.includeWhen(getStyleClass(), DROP_ABOVE_CLASS, dropAbove);
|
||||||
|
EasyBind.includeWhen(getStyleClass(), DROP_BELOW_CLASS, dropBelow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setDropPositionStyleClass(double verticalCursorPosition) {
|
||||||
boolean isUpperHalf = verticalCursorPosition < this.getHeight() / 2.0;
|
boolean isUpperHalf = verticalCursorPosition < this.getHeight() / 2.0;
|
||||||
final double topBorder = isUpperHalf ? DROP_LINE_WIDTH : 0.0;
|
if (isUpperHalf) {
|
||||||
final double bottomBorder = !isUpperHalf ? DROP_LINE_WIDTH : 0.0;
|
this.dropAbove.set(true);
|
||||||
final BorderWidths borderWidths = new BorderWidths(topBorder, 0.0, bottomBorder, 0.0);
|
this.dropBelow.set(false);
|
||||||
final BorderStroke dragStroke = new BorderStroke(DROP_LINE_COLOR, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, borderWidths, Insets.EMPTY);
|
} else {
|
||||||
final List<BorderStroke> strokes = new ArrayList<BorderStroke>(defaultBorderStrokes);
|
this.dropAbove.set(false);
|
||||||
strokes.add(0, dragStroke);
|
this.dropBelow.set(true);
|
||||||
return new Border(strokes, defaultBorderImages);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetDropPositionStyleClasses() {
|
||||||
|
this.dropAbove.set(false);
|
||||||
|
this.dropBelow.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onDragDetected(MouseEvent event) {
|
private void onDragDetected(MouseEvent event) {
|
||||||
@@ -82,7 +80,7 @@ class DraggableListCell<T> extends ListCell<T> {
|
|||||||
|
|
||||||
if (event.getGestureSource() instanceof DraggableListCell<?> && event.getGestureSource() != this && event.getDragboard().hasString()) {
|
if (event.getGestureSource() instanceof DraggableListCell<?> && event.getGestureSource() != this && event.getDragboard().hasString()) {
|
||||||
event.acceptTransferModes(TransferMode.MOVE);
|
event.acceptTransferModes(TransferMode.MOVE);
|
||||||
setBorder(createDropPositionBorder(event.getY()));
|
setDropPositionStyleClass(event.getY());
|
||||||
}
|
}
|
||||||
|
|
||||||
event.consume();
|
event.consume();
|
||||||
@@ -94,7 +92,7 @@ class DraggableListCell<T> extends ListCell<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.getGestureSource() instanceof DraggableListCell<?> && event.getGestureSource() != this && event.getDragboard().hasString()) {
|
if (event.getGestureSource() instanceof DraggableListCell<?> && event.getGestureSource() != this && event.getDragboard().hasString()) {
|
||||||
setBorder(createDropPositionBorder(event.getY()));
|
setDropPositionStyleClass(event.getY());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +102,7 @@ class DraggableListCell<T> extends ListCell<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.getGestureSource() instanceof DraggableListCell<?> && event.getGestureSource() != this && event.getDragboard().hasString()) {
|
if (event.getGestureSource() instanceof DraggableListCell<?> && event.getGestureSource() != this && event.getDragboard().hasString()) {
|
||||||
setBorder(new Border(defaultBorderStrokes, defaultBorderImages));
|
resetDropPositionStyleClasses();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package org.cryptomator.ui.controls;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inspired by de.jensd:fontawesomefx-fontawesome
|
||||||
|
*/
|
||||||
|
public enum FontAwesome5Icon {
|
||||||
|
ANCHOR("\uF13D"), //
|
||||||
|
ARROW_ALT_UP("\uF357"), //
|
||||||
|
CHECK("\uF00C"), //
|
||||||
|
COG("\uF013"), //
|
||||||
|
COGS("\uF085"), //
|
||||||
|
EXCLAMATION_TRIANGLE("\uF071"), //
|
||||||
|
EYE("\uF06E"), //
|
||||||
|
EYE_SLASH("\uF070"), //
|
||||||
|
FILE_IMPORT("\uF56F"), //
|
||||||
|
FOLDER_OPEN("\uF07C"), //
|
||||||
|
HDD("\uF0A0"), //
|
||||||
|
KEY("\uF084"), //
|
||||||
|
LOCK_ALT("\uF30D"), //
|
||||||
|
LOCK_OPEN_ALT("\uF3C2"), //
|
||||||
|
MINUS("\uF068"), //
|
||||||
|
PLUS("\uF067"), //
|
||||||
|
QUESTION("\uF128"), //
|
||||||
|
SPARKLES("\uF890"), //
|
||||||
|
SPINNER("\uF110"), //
|
||||||
|
SYNC("\uF021"), //
|
||||||
|
TIMES("\uF00D"), //
|
||||||
|
WRENCH("\uF0AD"), //
|
||||||
|
;
|
||||||
|
|
||||||
|
private final String unicode;
|
||||||
|
|
||||||
|
FontAwesome5Icon(String unicode) {
|
||||||
|
this.unicode = unicode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String unicode() {
|
||||||
|
return unicode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package org.cryptomator.ui.controls;
|
||||||
|
|
||||||
|
import javafx.beans.property.DoubleProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleDoubleProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.scene.text.Font;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
import org.cryptomator.ui.common.FontLoader;
|
||||||
|
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inspired by de.jensd:fontawesomefx-fontawesome
|
||||||
|
*/
|
||||||
|
public class FontAwesome5IconView extends Text {
|
||||||
|
|
||||||
|
private static final FontAwesome5Icon DEFAULT_GLYPH = FontAwesome5Icon.ANCHOR;
|
||||||
|
private static final double DEFAULT_GLYPH_SIZE = 12.0;
|
||||||
|
private static final String FONT_PATH = "/css/fontawesome5-pro-solid.otf";
|
||||||
|
private static final Font FONT;
|
||||||
|
|
||||||
|
private ObjectProperty<FontAwesome5Icon> glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH);
|
||||||
|
private DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE);
|
||||||
|
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
FONT = FontLoader.load(FONT_PATH);
|
||||||
|
} catch (FontLoader.FontLoaderException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public FontAwesome5IconView() {
|
||||||
|
getStyleClass().addAll("glyph-icon");
|
||||||
|
glyphProperty().addListener(this::glyphChanged);
|
||||||
|
glyphSizeProperty().addListener(this::glyphSizeChanged);
|
||||||
|
setFont(FONT);
|
||||||
|
setGlyph(DEFAULT_GLYPH);
|
||||||
|
setGlyphSize(DEFAULT_GLYPH_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void glyphChanged(@SuppressWarnings("unused") ObservableValue<? extends FontAwesome5Icon> observable, @SuppressWarnings("unused") FontAwesome5Icon oldValue, FontAwesome5Icon newValue) {
|
||||||
|
setText(newValue.unicode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void glyphSizeChanged(@SuppressWarnings("unused") ObservableValue<? extends Number> observable, @SuppressWarnings("unused") Number oldValue, Number newValue) {
|
||||||
|
setFont(new Font(FONT.getFamily(), newValue.doubleValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Getter/Setter */
|
||||||
|
|
||||||
|
public ObjectProperty<FontAwesome5Icon> glyphProperty() {
|
||||||
|
return glyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGlyph(FontAwesome5Icon glyph) {
|
||||||
|
this.glyph.set(glyph == null ? DEFAULT_GLYPH : glyph);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FontAwesome5Icon getGlyph() {
|
||||||
|
return glyph.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DoubleProperty glyphSizeProperty() {
|
||||||
|
return glyphSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGlyphSize(double glyphSize) {
|
||||||
|
this.glyphSize.set(glyphSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getGlyphSize() {
|
||||||
|
return glyphSize.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package org.cryptomator.ui.controls;
|
||||||
|
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.StringBinding;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
|
||||||
|
public class FormattedLabel extends Label {
|
||||||
|
|
||||||
|
private final StringProperty format = new SimpleStringProperty("");
|
||||||
|
private final ObjectProperty<Object> arg1 = new SimpleObjectProperty<>();
|
||||||
|
// TODO: add arg2, arg3, ... on demand
|
||||||
|
|
||||||
|
public FormattedLabel() {
|
||||||
|
textProperty().bind(createStringBinding());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected StringBinding createStringBinding() {
|
||||||
|
return Bindings.createStringBinding(this::updateText, format, arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String updateText() {
|
||||||
|
return String.format(format.get(), arg1.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Observables */
|
||||||
|
|
||||||
|
public StringProperty formatProperty() {
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFormat() {
|
||||||
|
return format.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFormat(String format) {
|
||||||
|
this.format.set(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<Object> arg1Property() {
|
||||||
|
return arg1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getArg1() {
|
||||||
|
return arg1.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setArg1(Object arg1) {
|
||||||
|
this.arg1.set(arg1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package org.cryptomator.ui.controls;
|
||||||
|
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.ContentDisplay;
|
||||||
|
import javafx.scene.control.ToggleButton;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
|
||||||
|
public class NiceSecurePasswordField extends StackPane {
|
||||||
|
|
||||||
|
private static final String STYLE_CLASS = "nice-secure-password-field";
|
||||||
|
private static final String ICONS_STLYE_CLASS = "icons";
|
||||||
|
private static final String REVEAL_BUTTON_STLYE_CLASS = "reveal-button";
|
||||||
|
private static final int ICON_SPACING = 6;
|
||||||
|
private static final double ICON_SIZE = 14.0;
|
||||||
|
|
||||||
|
private final SecurePasswordField passwordField = new SecurePasswordField();
|
||||||
|
private final FontAwesome5IconView capsLockedIcon = new FontAwesome5IconView();
|
||||||
|
private final FontAwesome5IconView nonPrintableCharsIcon = new FontAwesome5IconView();
|
||||||
|
private final FontAwesome5IconView revealPasswordIcon = new FontAwesome5IconView();
|
||||||
|
private final ToggleButton revealPasswordButton = new ToggleButton(null, revealPasswordIcon);
|
||||||
|
private final HBox iconContainer = new HBox(ICON_SPACING, nonPrintableCharsIcon, capsLockedIcon, revealPasswordButton);
|
||||||
|
|
||||||
|
public NiceSecurePasswordField() {
|
||||||
|
getStyleClass().add(STYLE_CLASS);
|
||||||
|
|
||||||
|
iconContainer.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
iconContainer.setMaxWidth(Double.NEGATIVE_INFINITY);
|
||||||
|
iconContainer.setPrefWidth(42); // TODO
|
||||||
|
iconContainer.getStyleClass().add(ICONS_STLYE_CLASS);
|
||||||
|
StackPane.setAlignment(iconContainer, Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
|
capsLockedIcon.setGlyph(FontAwesome5Icon.ARROW_ALT_UP);
|
||||||
|
capsLockedIcon.setGlyphSize(ICON_SIZE);
|
||||||
|
capsLockedIcon.visibleProperty().bind(passwordField.capsLockedProperty());
|
||||||
|
capsLockedIcon.managedProperty().bind(passwordField.capsLockedProperty());
|
||||||
|
|
||||||
|
nonPrintableCharsIcon.setGlyph(FontAwesome5Icon.EXCLAMATION_TRIANGLE);
|
||||||
|
nonPrintableCharsIcon.setGlyphSize(ICON_SIZE);
|
||||||
|
nonPrintableCharsIcon.visibleProperty().bind(passwordField.containingNonPrintableCharsProperty());
|
||||||
|
nonPrintableCharsIcon.managedProperty().bind(passwordField.containingNonPrintableCharsProperty());
|
||||||
|
|
||||||
|
revealPasswordIcon.setGlyph(FontAwesome5Icon.EYE);
|
||||||
|
revealPasswordIcon.glyphProperty().bind(Bindings.createObjectBinding(this::getRevealPasswordGlyph, revealPasswordButton.selectedProperty()));
|
||||||
|
revealPasswordIcon.setGlyphSize(ICON_SIZE);
|
||||||
|
|
||||||
|
revealPasswordButton.setContentDisplay(ContentDisplay.LEFT);
|
||||||
|
revealPasswordButton.setFocusTraversable(false);
|
||||||
|
revealPasswordButton.visibleProperty().bind(passwordField.focusedProperty());
|
||||||
|
revealPasswordButton.managedProperty().bind(passwordField.focusedProperty());
|
||||||
|
revealPasswordButton.getStyleClass().add(REVEAL_BUTTON_STLYE_CLASS);
|
||||||
|
|
||||||
|
passwordField.revealPasswordProperty().bind(revealPasswordButton.selectedProperty());
|
||||||
|
|
||||||
|
getChildren().addAll(passwordField, iconContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FontAwesome5Icon getRevealPasswordGlyph() {
|
||||||
|
return revealPasswordButton.isSelected() ? FontAwesome5Icon.EYE_SLASH : FontAwesome5Icon.EYE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Passthrough */
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requestFocus() {
|
||||||
|
passwordField.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty textProperty() {
|
||||||
|
return passwordField.textProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CharSequence getCharacters() {
|
||||||
|
return passwordField.getCharacters();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(char[] password) {
|
||||||
|
passwordField.setPassword(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void swipe() {
|
||||||
|
passwordField.swipe();;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void selectAll() {
|
||||||
|
passwordField.selectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void selectRange(int anchor, int caretPosition) {
|
||||||
|
passwordField.selectRange(anchor, caretPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package org.cryptomator.ui.controls;
|
||||||
|
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.control.TextFormatter;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class NumericTextField extends TextField {
|
||||||
|
|
||||||
|
private final static Pattern DIGIT_PATTERN = Pattern.compile("\\d*");
|
||||||
|
|
||||||
|
public NumericTextField() {
|
||||||
|
this.setTextFormatter(new TextFormatter<>(this::filterNumericTextChange));
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextFormatter.Change filterNumericTextChange(TextFormatter.Change change) {
|
||||||
|
return DIGIT_PATTERN.matcher(change.getText()).matches() ? change : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package org.cryptomator.ui.controls;
|
||||||
|
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
|
import org.fxmisc.easybind.EasyBind;
|
||||||
|
|
||||||
|
public class PasswordStrengthIndicator extends HBox {
|
||||||
|
|
||||||
|
private static final String STYLECLASS = "password-strength-indicator";
|
||||||
|
private static final String SEGMENT_CLASS = "segment";
|
||||||
|
private static final String ACTIVE_SEGMENT_CLASS = "active";
|
||||||
|
private static final String STRENGTH_0_CLASS = "strength-0";
|
||||||
|
private static final String STRENGTH_1_CLASS = "strength-1";
|
||||||
|
private static final String STRENGTH_2_CLASS = "strength-2";
|
||||||
|
private static final String STRENGTH_3_CLASS = "strength-3";
|
||||||
|
private static final String STRENGTH_4_CLASS = "strength-4";
|
||||||
|
|
||||||
|
private final Region s0;
|
||||||
|
private final Region s1;
|
||||||
|
private final Region s2;
|
||||||
|
private final Region s3;
|
||||||
|
private final Region s4;
|
||||||
|
private final IntegerProperty strength = new SimpleIntegerProperty();
|
||||||
|
private final BooleanBinding isStrength0 = strength.isEqualTo(0);
|
||||||
|
private final BooleanBinding isStrength1 = strength.isEqualTo(1);
|
||||||
|
private final BooleanBinding isStrength2 = strength.isEqualTo(2);
|
||||||
|
private final BooleanBinding isStrength3 = strength.isEqualTo(3);
|
||||||
|
private final BooleanBinding isStrength4 = strength.isEqualTo(4);
|
||||||
|
private final BooleanBinding isMinimumStrength0 = strength.greaterThanOrEqualTo(0);
|
||||||
|
private final BooleanBinding isMinimumStrength1 = strength.greaterThanOrEqualTo(1);
|
||||||
|
private final BooleanBinding isMinimumStrength2 = strength.greaterThanOrEqualTo(2);
|
||||||
|
private final BooleanBinding isMinimumStrength3 = strength.greaterThanOrEqualTo(3);
|
||||||
|
private final BooleanBinding isMinimumStrength4 = strength.greaterThanOrEqualTo(4);
|
||||||
|
|
||||||
|
public PasswordStrengthIndicator() {
|
||||||
|
this.s0 = new Region();
|
||||||
|
this.s1 = new Region();
|
||||||
|
this.s2 = new Region();
|
||||||
|
this.s3 = new Region();
|
||||||
|
this.s4 = new Region();
|
||||||
|
|
||||||
|
getChildren().addAll(s0, s1, s2, s3, s4);
|
||||||
|
setHgrow(s0, Priority.ALWAYS);
|
||||||
|
setHgrow(s1, Priority.ALWAYS);
|
||||||
|
setHgrow(s2, Priority.ALWAYS);
|
||||||
|
setHgrow(s3, Priority.ALWAYS);
|
||||||
|
setHgrow(s4, Priority.ALWAYS);
|
||||||
|
|
||||||
|
getStyleClass().add(STYLECLASS);
|
||||||
|
s0.getStyleClass().add(SEGMENT_CLASS);
|
||||||
|
s1.getStyleClass().add(SEGMENT_CLASS);
|
||||||
|
s2.getStyleClass().add(SEGMENT_CLASS);
|
||||||
|
s3.getStyleClass().add(SEGMENT_CLASS);
|
||||||
|
s4.getStyleClass().add(SEGMENT_CLASS);
|
||||||
|
|
||||||
|
EasyBind.includeWhen(s0.getStyleClass(), ACTIVE_SEGMENT_CLASS, isMinimumStrength0);
|
||||||
|
EasyBind.includeWhen(s1.getStyleClass(), ACTIVE_SEGMENT_CLASS, isMinimumStrength1);
|
||||||
|
EasyBind.includeWhen(s2.getStyleClass(), ACTIVE_SEGMENT_CLASS, isMinimumStrength2);
|
||||||
|
EasyBind.includeWhen(s3.getStyleClass(), ACTIVE_SEGMENT_CLASS, isMinimumStrength3);
|
||||||
|
EasyBind.includeWhen(s4.getStyleClass(), ACTIVE_SEGMENT_CLASS, isMinimumStrength4);
|
||||||
|
EasyBind.includeWhen(getStyleClass(), STRENGTH_0_CLASS, isStrength0);
|
||||||
|
EasyBind.includeWhen(getStyleClass(), STRENGTH_1_CLASS, isStrength1);
|
||||||
|
EasyBind.includeWhen(getStyleClass(), STRENGTH_2_CLASS, isStrength2);
|
||||||
|
EasyBind.includeWhen(getStyleClass(), STRENGTH_3_CLASS, isStrength3);
|
||||||
|
EasyBind.includeWhen(getStyleClass(), STRENGTH_4_CLASS, isStrength4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Observables */
|
||||||
|
|
||||||
|
public IntegerProperty strengthProperty() {
|
||||||
|
return strength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStrength(int strength) {
|
||||||
|
this.strength.set(strength);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getStrength() {
|
||||||
|
return strength.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
+91
-69
@@ -11,20 +11,19 @@ package org.cryptomator.ui.controls;
|
|||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import javafx.beans.NamedArg;
|
import javafx.beans.NamedArg;
|
||||||
import javafx.beans.Observable;
|
import javafx.beans.Observable;
|
||||||
import javafx.geometry.Insets;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.geometry.Pos;
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
import javafx.scene.control.Label;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.scene.control.OverrunStyle;
|
import javafx.scene.AccessibleAttribute;
|
||||||
|
import javafx.scene.AccessibleRole;
|
||||||
|
import javafx.scene.control.IndexRange;
|
||||||
import javafx.scene.control.PasswordField;
|
import javafx.scene.control.PasswordField;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.input.DragEvent;
|
import javafx.scene.input.DragEvent;
|
||||||
import javafx.scene.input.Dragboard;
|
import javafx.scene.input.Dragboard;
|
||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.input.KeyCode;
|
||||||
import javafx.scene.input.KeyEvent;
|
import javafx.scene.input.KeyEvent;
|
||||||
import javafx.scene.input.TransferMode;
|
import javafx.scene.input.TransferMode;
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.text.Font;
|
|
||||||
import javafx.scene.text.Text;
|
|
||||||
|
|
||||||
import java.awt.Toolkit;
|
import java.awt.Toolkit;
|
||||||
import java.nio.CharBuffer;
|
import java.nio.CharBuffer;
|
||||||
@@ -33,54 +32,54 @@ import java.text.Normalizer.Form;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patched PasswordField that doesn't create String copies of the password in memory. Instead the password is stored in a char[] that can be swiped.
|
* Patched PasswordField that doesn't create String copies of the password in memory (unless explicitly revealed). Instead the password is stored in a char[] that can be swiped.
|
||||||
*
|
*
|
||||||
* @implNote Since {@link #setText(String)} is final, we can not override its behaviour. For that reason you should not use the {@link #textProperty()} for anything else than display purposes.
|
* @implNote Since {@link #setText(String)} is final, we can not override its behaviour. For that reason you should not use the {@link #textProperty()} for anything else than display purposes.
|
||||||
*/
|
*/
|
||||||
public class SecPasswordField extends PasswordField {
|
public class SecurePasswordField extends TextField {
|
||||||
|
|
||||||
private static final char SWIPE_CHAR = ' ';
|
private static final char SWIPE_CHAR = ' ';
|
||||||
private static final int INITIAL_BUFFER_SIZE = 50;
|
private static final int INITIAL_BUFFER_SIZE = 50;
|
||||||
private static final int GROW_BUFFER_SIZE = 50;
|
private static final int GROW_BUFFER_SIZE = 50;
|
||||||
private static final String PLACEHOLDER = "*";
|
private static final String DEFAULT_PLACEHOLDER = "●";
|
||||||
private static final double PADDING = 2.0;
|
private static final String STYLE_CLASS = "secure-password-field";
|
||||||
private static final double INDICATOR_PADDING = 4.0;
|
|
||||||
private static final Color INDICATOR_COLOR = new Color(0.901, 0.494, 0.133, 1.0);
|
|
||||||
|
|
||||||
private final Tooltip tooltip = new Tooltip();
|
private final String placeholderChar;
|
||||||
private final Label indicator = new Label();
|
private final BooleanProperty capsLocked = new SimpleBooleanProperty();
|
||||||
private final String nonPrintableCharsWarning;
|
private final BooleanProperty containingNonPrintableChars = new SimpleBooleanProperty();
|
||||||
private final String capslockWarning;
|
private final BooleanProperty revealPassword = new SimpleBooleanProperty();
|
||||||
|
|
||||||
private char[] content = new char[INITIAL_BUFFER_SIZE];
|
private char[] content = new char[INITIAL_BUFFER_SIZE];
|
||||||
private int length = 0;
|
private int length = 0;
|
||||||
|
|
||||||
public SecPasswordField() {
|
public SecurePasswordField() {
|
||||||
this("", "");
|
this(DEFAULT_PLACEHOLDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SecPasswordField(@NamedArg("nonPrintableCharsWarning") String nonPrintableCharsWarning, @NamedArg("capslockWarning") String capslockWarning) {
|
public SecurePasswordField(@NamedArg("placeholderChar") String placeholderChar) {
|
||||||
this.nonPrintableCharsWarning = nonPrintableCharsWarning;
|
this.getStyleClass().add(STYLE_CLASS);
|
||||||
this.capslockWarning = capslockWarning;
|
this.placeholderChar = placeholderChar;
|
||||||
indicator.setPadding(new Insets(PADDING, INDICATOR_PADDING, PADDING, INDICATOR_PADDING));
|
this.setAccessibleRole(AccessibleRole.PASSWORD_FIELD);
|
||||||
indicator.setAlignment(Pos.CENTER_RIGHT);
|
|
||||||
indicator.setMouseTransparent(true);
|
|
||||||
indicator.setTextOverrun(OverrunStyle.CLIP);
|
|
||||||
indicator.setTextFill(INDICATOR_COLOR);
|
|
||||||
indicator.setFont(Font.font(indicator.getFont().getFamily(), 15.0));
|
|
||||||
this.getChildren().add(indicator);
|
|
||||||
this.setTooltip(tooltip);
|
|
||||||
this.addEventHandler(DragEvent.DRAG_OVER, this::handleDragOver);
|
this.addEventHandler(DragEvent.DRAG_OVER, this::handleDragOver);
|
||||||
this.addEventHandler(DragEvent.DRAG_DROPPED, this::handleDragDropped);
|
this.addEventHandler(DragEvent.DRAG_DROPPED, this::handleDragDropped);
|
||||||
this.addEventHandler(KeyEvent.ANY, this::handleKeyEvent);
|
this.addEventHandler(KeyEvent.ANY, this::handleKeyEvent);
|
||||||
|
this.revealPasswordProperty().addListener(this::revealPasswordChanged);
|
||||||
this.focusedProperty().addListener(this::focusedChanged);
|
this.focusedProperty().addListener(this::focusedChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void cut() {
|
||||||
protected void layoutChildren() {
|
}
|
||||||
super.layoutChildren();
|
|
||||||
indicator.relocate(0.0, 0.0);
|
public void copy() {
|
||||||
indicator.resize(getWidth(), getHeight());
|
}
|
||||||
|
|
||||||
|
public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
|
||||||
|
switch(attribute) {
|
||||||
|
case TEXT:
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
return super.queryAccessibleAttribute(attribute, parameters);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleDragOver(DragEvent event) {
|
private void handleDragOver(DragEvent event) {
|
||||||
@@ -101,42 +100,32 @@ public class SecPasswordField extends PasswordField {
|
|||||||
|
|
||||||
private void handleKeyEvent(KeyEvent e) {
|
private void handleKeyEvent(KeyEvent e) {
|
||||||
if (e.getCode() == KeyCode.CAPS) {
|
if (e.getCode() == KeyCode.CAPS) {
|
||||||
updateVisualHints(true);
|
updateCapsLocked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void revealPasswordChanged(@SuppressWarnings("unused") Observable observable) {
|
||||||
|
IndexRange selection = getSelection();
|
||||||
|
if (isRevealPassword()) {
|
||||||
|
super.setText(this.getCharacters().toString());
|
||||||
|
} else {
|
||||||
|
String placeholderText = Strings.repeat(placeholderChar, length);
|
||||||
|
super.setText(placeholderText);
|
||||||
|
}
|
||||||
|
selectRange(selection.getStart(), selection.getEnd());
|
||||||
|
}
|
||||||
|
|
||||||
private void focusedChanged(@SuppressWarnings("unused") Observable observable) {
|
private void focusedChanged(@SuppressWarnings("unused") Observable observable) {
|
||||||
updateVisualHints(isFocused());
|
updateCapsLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateVisualHints(boolean focused) {
|
private void updateCapsLocked() {
|
||||||
StringBuilder tooltipSb = new StringBuilder();
|
|
||||||
StringBuilder indicatorSb = new StringBuilder();
|
|
||||||
if (containsNonPrintableCharacters()) {
|
|
||||||
indicatorSb.append('⚠');
|
|
||||||
tooltipSb.append("- ").append(nonPrintableCharsWarning).append('\n');
|
|
||||||
}
|
|
||||||
// AWT code needed until https://bugs.openjdk.java.net/browse/JDK-8090882 is closed:
|
// AWT code needed until https://bugs.openjdk.java.net/browse/JDK-8090882 is closed:
|
||||||
if (focused && Toolkit.getDefaultToolkit().getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK)) {
|
capsLocked.set(isFocused() && Toolkit.getDefaultToolkit().getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK));
|
||||||
indicatorSb.append('⇪');
|
|
||||||
tooltipSb.append("- ").append(capslockWarning).append('\n');
|
|
||||||
}
|
|
||||||
indicator.setText(indicatorSb.toString());
|
|
||||||
if (!indicator.getText().isEmpty()) {
|
|
||||||
setPadding(new Insets(PADDING, getIndicatorWidth(), PADDING, PADDING));
|
|
||||||
} else {
|
|
||||||
setPadding(new Insets(PADDING));
|
|
||||||
}
|
|
||||||
tooltip.setText(tooltipSb.toString());
|
|
||||||
if (tooltip.getText().isEmpty()) {
|
|
||||||
setTooltip(null);
|
|
||||||
} else {
|
|
||||||
setTooltip(tooltip);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private double getIndicatorWidth() {
|
private void updateContainingNonPrintableChars() {
|
||||||
return new Text(indicator.getText()).getLayoutBounds().getWidth() + INDICATOR_PADDING * 2.0;
|
containingNonPrintableChars.set(containsNonPrintableCharacters());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -184,9 +173,13 @@ public class SecPasswordField extends PasswordField {
|
|||||||
normalizedText.getChars(0, normalizedText.length(), content, start);
|
normalizedText.getChars(0, normalizedText.length(), content, start);
|
||||||
|
|
||||||
// trigger visual hints
|
// trigger visual hints
|
||||||
updateVisualHints(true);
|
updateContainingNonPrintableChars();
|
||||||
String placeholderString = Strings.repeat(PLACEHOLDER, normalizedText.length());
|
if (isRevealPassword()) {
|
||||||
super.replaceText(start, end, placeholderString);
|
super.replaceText(start, end, text);
|
||||||
|
} else {
|
||||||
|
String placeholderString = Strings.repeat(placeholderChar, normalizedText.length());
|
||||||
|
super.replaceText(start, end, placeholderString);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void growContentIfNeeded() {
|
private void growContentIfNeeded() {
|
||||||
@@ -201,9 +194,9 @@ public class SecPasswordField extends PasswordField {
|
|||||||
/**
|
/**
|
||||||
* Creates a CharSequence by wrapping the password characters.
|
* Creates a CharSequence by wrapping the password characters.
|
||||||
*
|
*
|
||||||
* @return A character sequence backed by the SecPasswordField's buffer (not a copy).
|
* @return A character sequence backed by the SecurePasswordField's buffer (not a copy).
|
||||||
* @implNote The CharSequence will not copy the backing char[].
|
* @implNote The CharSequence will not copy the backing char[].
|
||||||
* Therefore any mutation to the SecPasswordField's content will mutate or eventually swipe the returned CharSequence.
|
* Therefore any mutation to the SecurePasswordField's content will mutate or eventually swipe the returned CharSequence.
|
||||||
* @implSpec The CharSequence is usually in <a href="https://www.unicode.org/glossary/#normalization_form_c">NFC</a> representation (unless NFD-encoded char[] is set via {@link #setPassword(char[])}).
|
* @implSpec The CharSequence is usually in <a href="https://www.unicode.org/glossary/#normalization_form_c">NFC</a> representation (unless NFD-encoded char[] is set via {@link #setPassword(char[])}).
|
||||||
* @see #swipe()
|
* @see #swipe()
|
||||||
*/
|
*/
|
||||||
@@ -239,7 +232,7 @@ public class SecPasswordField extends PasswordField {
|
|||||||
content = Arrays.copyOf(password, password.length);
|
content = Arrays.copyOf(password, password.length);
|
||||||
length = password.length;
|
length = password.length;
|
||||||
|
|
||||||
String placeholderString = Strings.repeat(PLACEHOLDER, password.length);
|
String placeholderString = Strings.repeat(placeholderChar, password.length);
|
||||||
setText(placeholderString);
|
setText(placeholderString);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,4 +249,33 @@ public class SecPasswordField extends PasswordField {
|
|||||||
Arrays.fill(buffer, SWIPE_CHAR);
|
Arrays.fill(buffer, SWIPE_CHAR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Observable Properties */
|
||||||
|
|
||||||
|
public ReadOnlyBooleanProperty capsLockedProperty() {
|
||||||
|
return capsLocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCapsLocked() {
|
||||||
|
return capsLocked.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlyBooleanProperty containingNonPrintableCharsProperty() {
|
||||||
|
return containingNonPrintableChars;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isContainingNonPrintableChars() {
|
||||||
|
return containingNonPrintableChars.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanProperty revealPasswordProperty() {
|
||||||
|
return revealPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRevealPassword() {
|
||||||
|
return revealPassword.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRevealPassword(boolean revealPassword) {
|
||||||
|
this.revealPassword.set(revealPassword);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package org.cryptomator.ui.controls;
|
||||||
|
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.StringBinding;
|
||||||
|
import javafx.beans.property.LongProperty;
|
||||||
|
import javafx.beans.property.SimpleLongProperty;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
|
||||||
|
public class ThrougputLabel extends Label {
|
||||||
|
|
||||||
|
private static final long kibsThreshold = 1l << 7; // 0.128 kiB/s
|
||||||
|
private static final long mibsThreshold = 1l << 19; // 0.512 MiB/s
|
||||||
|
|
||||||
|
private final StringProperty idleFormat = new SimpleStringProperty("-");
|
||||||
|
private final StringProperty kibsFormat = new SimpleStringProperty("%.3f");
|
||||||
|
private final StringProperty mibsFormat = new SimpleStringProperty("%.3f");
|
||||||
|
private final LongProperty bytesPerSecond = new SimpleLongProperty();
|
||||||
|
|
||||||
|
public ThrougputLabel() {
|
||||||
|
textProperty().bind(createStringBinding());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected StringBinding createStringBinding() {
|
||||||
|
return Bindings.createStringBinding(this::updateText, kibsFormat, mibsFormat, bytesPerSecond);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String updateText() {
|
||||||
|
long bps = bytesPerSecond.get();
|
||||||
|
if (bps > mibsThreshold) {
|
||||||
|
double mibs = ((double) bytesPerSecond.get()) / 1024.0 / 1024.0;
|
||||||
|
return String.format(mibsFormat.get(), mibs);
|
||||||
|
} else if (bps > kibsThreshold) {
|
||||||
|
double kibs = ((double) bytesPerSecond.get()) / 1024.0;
|
||||||
|
return String.format(kibsFormat.get(), kibs);
|
||||||
|
} else {
|
||||||
|
return String.format(idleFormat.get(), bps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Observables */
|
||||||
|
|
||||||
|
public StringProperty idleFormatProperty() {
|
||||||
|
return idleFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIdleFormat() {
|
||||||
|
return idleFormat.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIdleFormat(String idleFormat) {
|
||||||
|
this.idleFormat.set(idleFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty kibsFormatProperty() {
|
||||||
|
return kibsFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKibsFormat() {
|
||||||
|
return kibsFormat.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKibsFormat(String kibsFormat) {
|
||||||
|
this.kibsFormat.set(kibsFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty mibsFormatProperty() {
|
||||||
|
return mibsFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMibsFormat() {
|
||||||
|
return mibsFormat.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMibsFormat(String mibsFormat) {
|
||||||
|
this.mibsFormat.set(mibsFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LongProperty bytesPerSecondProperty() {
|
||||||
|
return bytesPerSecond;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getBytesPerSecond() {
|
||||||
|
return bytesPerSecond.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBytesPerSecond(long bytesPerSecond) {
|
||||||
|
this.bytesPerSecond.set(bytesPerSecond);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package org.cryptomator.ui.forgetPassword;
|
||||||
|
|
||||||
|
import dagger.BindsInstance;
|
||||||
|
import dagger.Lazy;
|
||||||
|
import dagger.Subcomponent;
|
||||||
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
|
||||||
|
import javax.inject.Named;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
@ForgetPasswordScoped
|
||||||
|
@Subcomponent(modules = {ForgetPasswordModule.class})
|
||||||
|
public interface ForgetPasswordComponent {
|
||||||
|
|
||||||
|
@ForgetPasswordWindow
|
||||||
|
ReadOnlyBooleanProperty confirmedProperty();
|
||||||
|
|
||||||
|
@ForgetPasswordWindow
|
||||||
|
Stage window();
|
||||||
|
|
||||||
|
@FxmlScene(FxmlFile.FORGET_PASSWORD)
|
||||||
|
Lazy<Scene> scene();
|
||||||
|
|
||||||
|
default CompletableFuture<Boolean> showForgetPassword() {
|
||||||
|
CompletableFuture<Boolean> result = new CompletableFuture<>();
|
||||||
|
Stage stage = window();
|
||||||
|
stage.setScene(scene().get());
|
||||||
|
stage.sizeToScene();
|
||||||
|
stage.show();
|
||||||
|
stage.setOnHidden(evt -> result.complete(confirmedProperty().get()));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subcomponent.Builder
|
||||||
|
interface Builder {
|
||||||
|
|
||||||
|
@BindsInstance
|
||||||
|
Builder vault(@ForgetPasswordWindow Vault vault);
|
||||||
|
|
||||||
|
@BindsInstance
|
||||||
|
Builder owner(@Named("forgetPasswordOwner") Stage owner);
|
||||||
|
|
||||||
|
ForgetPasswordComponent build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package org.cryptomator.ui.forgetPassword;
|
||||||
|
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.keychain.KeychainAccess;
|
||||||
|
import org.cryptomator.keychain.KeychainAccessException;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ForgetPasswordScoped
|
||||||
|
public class ForgetPasswordController implements FxController {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ForgetPasswordController.class);
|
||||||
|
|
||||||
|
private final Stage window;
|
||||||
|
private final Vault vault;
|
||||||
|
private final Optional<KeychainAccess> keychainAccess;
|
||||||
|
private final BooleanProperty confirmedResult;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ForgetPasswordController(@ForgetPasswordWindow Stage window, @ForgetPasswordWindow Vault vault, Optional<KeychainAccess> keychainAccess, @ForgetPasswordWindow BooleanProperty confirmedResult) {
|
||||||
|
this.window = window;
|
||||||
|
this.vault = vault;
|
||||||
|
this.keychainAccess = keychainAccess;
|
||||||
|
this.confirmedResult = confirmedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void close() {
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void finish() {
|
||||||
|
if (keychainAccess.isPresent()) {
|
||||||
|
try {
|
||||||
|
keychainAccess.get().deletePassphrase(vault.getId());
|
||||||
|
LOG.debug("Forgot password for vault {}.", vault.getDisplayableName());
|
||||||
|
confirmedResult.setValue(true);
|
||||||
|
} catch (KeychainAccessException e) {
|
||||||
|
LOG.error("Failed to remove entry from system keychain.", e);
|
||||||
|
confirmedResult.setValue(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package org.cryptomator.ui.forgetPassword;
|
||||||
|
|
||||||
|
import dagger.Binds;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import dagger.multibindings.IntoMap;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.cryptomator.ui.common.FXMLLoaderFactory;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.FxControllerKey;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
abstract class ForgetPasswordModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@ForgetPasswordWindow
|
||||||
|
@ForgetPasswordScoped
|
||||||
|
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
|
||||||
|
return new FXMLLoaderFactory(factories, resourceBundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@ForgetPasswordWindow
|
||||||
|
@ForgetPasswordScoped
|
||||||
|
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon, @Named("forgetPasswordOwner") Stage owner) {
|
||||||
|
Stage stage = new Stage();
|
||||||
|
stage.setTitle(resourceBundle.getString("forgetPassword.title"));
|
||||||
|
stage.setResizable(false);
|
||||||
|
stage.initModality(Modality.WINDOW_MODAL);
|
||||||
|
stage.initOwner(owner);
|
||||||
|
windowIcon.ifPresent(stage.getIcons()::add);
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@FxmlScene(FxmlFile.FORGET_PASSWORD)
|
||||||
|
@ForgetPasswordScoped
|
||||||
|
static Scene provideForgetPasswordScene(@ForgetPasswordWindow FXMLLoaderFactory fxmlLoaders, @ForgetPasswordWindow Stage window) {
|
||||||
|
return fxmlLoaders.createScene("/fxml/forget_password.fxml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@ForgetPasswordWindow
|
||||||
|
@ForgetPasswordScoped
|
||||||
|
static BooleanProperty provideConfirmedProperty() {
|
||||||
|
return new SimpleBooleanProperty(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@ForgetPasswordWindow
|
||||||
|
@ForgetPasswordScoped
|
||||||
|
abstract ReadOnlyBooleanProperty bindReadOnlyConfirmedProperty(@ForgetPasswordWindow BooleanProperty confirmedProperty);
|
||||||
|
|
||||||
|
// ------------------
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(ForgetPasswordController.class)
|
||||||
|
abstract FxController bindForgetPasswordController(ForgetPasswordController controller);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user