Compare commits
230 Commits
Author | SHA1 | Date | |
---|---|---|---|
837ed0106f
|
|||
4a0bcf9762 | |||
829e94fa5f | |||
c7ee545ee2 | |||
d70a848ef3
|
|||
d1d52468bc | |||
ede50ed3e5
|
|||
61fbeda05e
|
|||
5daff3620e
|
|||
618a4aa3cf
|
|||
108db1ae11
|
|||
6d7afbaa8f
|
|||
86e189a40a
|
|||
0efd1e5594
|
|||
f6eeeee79b
|
|||
8eb7743057
|
|||
f0e645c0ae | |||
af219274f5
|
|||
05d4917bb2
|
|||
f02b01291b | |||
84d80982e5
|
|||
2d9283551a
|
|||
758e52e030 | |||
b9e19d69b9 | |||
c6819e637b
|
|||
41f07dc452 | |||
9419ba2ee8 | |||
f36f330c81
|
|||
5b4f2762e5
|
|||
1b60ab3f0d | |||
8ed6faca96 | |||
52d6282e13
|
|||
0dbd15e958 | |||
d8ae8a65b8 | |||
a12d765494 | |||
3cd9d76d2c
|
|||
d394c2d058
|
|||
7cc4928826
|
|||
4959bc9634
|
|||
16a0786d54
|
|||
40447f3f42
|
|||
be945fe3ee
|
|||
a8aa1c9ea7
|
|||
fd21c5789f
|
|||
1ccf4354aa | |||
cb2a3a6540
|
|||
3e594c1fbd
|
|||
f21d077522
|
|||
31cb22035b
|
|||
ec6b67099f
|
|||
89b9afb3db
|
|||
f98811c899 | |||
920dcb53fc | |||
4ba85f68ef | |||
e06dd7dd57 | |||
c21da25789
|
|||
8a01229855 | |||
763830c727
|
|||
8829f267ec | |||
465ed20efa
|
|||
69ea737361 | |||
74a1f8232b
|
|||
9b6d0f3c97
|
|||
ff1891108e
|
|||
78573399e9
|
|||
beb0f3e469 | |||
dd2e09b6dc | |||
cf401d201c | |||
aa992e2bcf | |||
63ed1c480d | |||
3f3c561e25 | |||
fcd5767c4b | |||
d97af36ae1 | |||
d0c8c685ab | |||
8b204b3715 | |||
efbca9cbc9 | |||
661823219c | |||
9f517cfc6b | |||
ee0d70647c | |||
88f28e60f1 | |||
9bd06336eb | |||
dc114e5b3c | |||
f6c62f9073 | |||
4137bf393a | |||
dc58290f22 | |||
74025c6111 | |||
6c32cf650e | |||
f86f3ec200 | |||
f581b83359 | |||
b7ea7f0e85 | |||
e7d85bd968 | |||
15265d2b7c | |||
78ade078d4 | |||
5f3e615641 | |||
572541e381 | |||
f6c3da394d | |||
da309098b7 | |||
1983cebde1 | |||
46a883dda9 | |||
a6e5b3d77d | |||
ddbf9acd07 | |||
1d03128744 | |||
72ffa71d6b | |||
14ccf4ce58 | |||
bd75da1ab9 | |||
f77795edb1 | |||
dbf69c7cc1 | |||
d0f125f058 | |||
b4397fe2f2 | |||
1fe83dbcc0 | |||
c784ebb787 | |||
eb4e421974 | |||
4bbc4189ec | |||
19dcb2bea8 | |||
2cb124505d | |||
cb95c40ad6 | |||
b081960a31 | |||
f4a3bfed97 | |||
ecede45360 | |||
5acbd3b6e1 | |||
33aa851090 | |||
2491812ba0 | |||
71bb329857 | |||
dee317c27d | |||
c3dfedc642 | |||
a1d09d6550 | |||
0901f900e7 | |||
56bb00cd32 | |||
fe4f9bf219 | |||
209262b4c9 | |||
3fdbbfd756 | |||
0d77fbf831 | |||
59188711b8 | |||
74ebd158f2 | |||
719aa4cd4f | |||
498f3ef43d | |||
b02c2fdc65 | |||
b678ae295b | |||
3cbe3b5045 | |||
268e4439d7 | |||
98ebb321ce | |||
9234e23fae | |||
3e7a949be5 | |||
0167af54b0 | |||
517c840487 | |||
e216152e6b | |||
63f42ab8d9 | |||
1cdad2df0b | |||
5a5e6e2086 | |||
e382a86623 | |||
6f8859c3fd | |||
72d1e074f4 | |||
cd2e739529 | |||
8d81b76bad | |||
c34457730f | |||
00fc160550 | |||
9d7f85c58d | |||
4d4de3a27f | |||
8718596be2 | |||
9a947739a6 | |||
2ffcad9d35 | |||
59354c403d | |||
07fbe3438a | |||
2ed30c56cd | |||
e49d390089 | |||
d3c2eb4ff7 | |||
42184c47f7 | |||
6a1a9ecdbb | |||
f1856534c6 | |||
9ea8d24ab6 | |||
38c57c997f | |||
7bf35977f0 | |||
5d2a3b83d2 | |||
9e427e1ec3 | |||
ebfe603bc7 | |||
60791f2913 | |||
5d03d0f0eb | |||
79a121b6b5 | |||
e00fa592d6 | |||
a283217308 | |||
145ec06f57 | |||
01f81fadac | |||
e51d2946d0 | |||
1a17448724 | |||
0674035183 | |||
fdbec3d652 | |||
5ce62c10ca | |||
fa7be8c343 | |||
282db47153 | |||
381740e087 | |||
da77afdc32 | |||
2e42da87ec | |||
2e45e375b1 | |||
2e4a17c6c5 | |||
b4225b0d80 | |||
f135a99fdd | |||
698e260746 | |||
47ab5d1e0c | |||
71145bbb24 | |||
62d9df7ae8 | |||
b88f260efc | |||
e104a1f9b4 | |||
7b693e0328 | |||
afcf1e48a4 | |||
a21a5c8588 | |||
00603bedf6 | |||
96bfe489da | |||
698b57d99d | |||
c71c038317 | |||
43c1edae39 | |||
176f6c6463 | |||
bf499da97d | |||
c0f4a8e212 | |||
fb4fd85fe4 | |||
bc355f190f | |||
a76c2a347e | |||
07c4ccf3c8 | |||
e7e4c5af42 | |||
1e63c1a7d1 | |||
c5094e52cd | |||
9a9a475c0e | |||
f608b2d6ec | |||
abd0113588 | |||
ba336908d1 | |||
4bc393b055 | |||
bdd1b40107 | |||
0267a7bbab | |||
a437fb25da | |||
659a468049 | |||
062c9f418d |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -1 +0,0 @@
|
|||||||
* @CyB3RC0nN0R
|
|
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,32 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: bug
|
|
||||||
assignees: CyB3RC0nN0R, delvh, DieGurke, derharry333
|
|
||||||
projects: Envoy
|
|
||||||
milestone: Envoy v0.2-beta
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
|
||||||
- OS: [e.g. Debian GNU/Linux, Microsoft Windows 10]
|
|
||||||
- Version [e.g. 0.1-beta]
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: enhancement, feature
|
|
||||||
assignees: CyB3RC0nN0R, delvh, DieGurke
|
|
||||||
project: Envoy
|
|
||||||
milestones: Envoy v0.2-beta
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
10
.github/PULL_REQUEST_TEMPLATE/bugfix.md
vendored
10
.github/PULL_REQUEST_TEMPLATE/bugfix.md
vendored
@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug fix
|
|
||||||
title: Fixed Bug
|
|
||||||
labels: bug
|
|
||||||
assignees: CyB3RC0nN0R, delvh, DieGurke
|
|
||||||
reviewers: CyB3RC0nN0R, delvh
|
|
||||||
projects: Envoy
|
|
||||||
milestone: Envoy v0.1-beta
|
|
||||||
---
|
|
||||||
Fixes #{issue}
|
|
@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature integration
|
|
||||||
title: Added feature
|
|
||||||
labels: feature
|
|
||||||
assignees: CyB3RC0nN0R, delvh, DieGurke
|
|
||||||
reviewers: CyB3RC0nN0R, delvh
|
|
||||||
projects: Envoy
|
|
||||||
milestone: Envoy v0.1-beta
|
|
||||||
---
|
|
10
.github/PULL_REQUEST_TEMPLATE/javadoc_update.md
vendored
10
.github/PULL_REQUEST_TEMPLATE/javadoc_update.md
vendored
@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
name: Updated Javadoc
|
|
||||||
title: Updated Javadoc
|
|
||||||
labels: documentation
|
|
||||||
assignees: CyB3RC0nN0R, delvh
|
|
||||||
reviewers: CyB3RC0nN0R, delvh
|
|
||||||
projects: Envoy
|
|
||||||
milestone: Envoy v0.1-beta
|
|
||||||
|
|
||||||
---
|
|
32
.github/workflows/maven.yml
vendored
32
.github/workflows/maven.yml
vendored
@ -1,32 +0,0 @@
|
|||||||
name: Java CI
|
|
||||||
|
|
||||||
on: [push]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build & Package
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, windows-latest]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Set up JDK 11
|
|
||||||
uses: actions/setup-java@v1
|
|
||||||
with:
|
|
||||||
java-version: 11
|
|
||||||
- name: Cache Maven packages
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.m2
|
|
||||||
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
|
||||||
restore-keys: ${{ runner.os }}-m2
|
|
||||||
- name: Build with Maven
|
|
||||||
run: mvn -B package
|
|
||||||
- uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: envoy-${{ matrix.os }}
|
|
||||||
path: |
|
|
||||||
server/target/envoy-server-jar-with-dependencies.jar
|
|
||||||
client/target/envoy-client*shaded.jar
|
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -1 +1,8 @@
|
|||||||
|
# build folders
|
||||||
target/
|
target/
|
||||||
|
|
||||||
|
# Eclipse settings
|
||||||
|
/.settings
|
||||||
|
|
||||||
|
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||||
|
hs_err_pid*.log
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
eclipse.preferences.version=1
|
|
||||||
encoding/<project>=UTF-8
|
|
52
README.md
Normal file
52
README.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# Envoy
|
||||||
|
|
||||||
|
<img src="https://git.kske.dev/repo-avatars/33-31e14133097b01b748ab361e3c3adb47" style="display: block" width="150" height="150">
|
||||||
|
|
||||||
|
Envoy is a messenger written in Java.
|
||||||
|
On this page, the project is explained for different user groups.
|
||||||
|
|
||||||
|
## Regular User
|
||||||
|
|
||||||
|
To use Envoy to join an existing server, download the client from the [release page](https://git.kske.dev/zdm/envoy/releases).
|
||||||
|
|
||||||
|
When starting it for the first time, you can register yourself at a server of your choice.
|
||||||
|
After connecting to the server, you can add other users to your contact list and send them messages.
|
||||||
|
|
||||||
|
To chat with multiple users at once, you can create a group.
|
||||||
|
If you want to transfer a file to another user, you can attach it to a message.
|
||||||
|
|
||||||
|
On the settings page some convenience features can be configured, as well as the color theme.
|
||||||
|
|
||||||
|
## Server Administrator
|
||||||
|
|
||||||
|
To set up an Envoy server, download the package from the release page.
|
||||||
|
|
||||||
|
Because the project lacks external documentation for the moment, please refer to the Javadoc inside the source code to configure your Envoy instance.
|
||||||
|
|
||||||
|
## Programmer
|
||||||
|
|
||||||
|
Envoy is organized as a Maven project that is split into three modules.
|
||||||
|
|
||||||
|
### Client
|
||||||
|
|
||||||
|
* Sending and receiving of messages, groups, sending images and voice messages
|
||||||
|
* User interface (UI)
|
||||||
|
* Client configuration
|
||||||
|
* Advanced logging possibilities
|
||||||
|
* Tons of Events to interact with
|
||||||
|
* Detailed Javadoc to improve readability of code
|
||||||
|
|
||||||
|
### Common
|
||||||
|
* Basic datatypes
|
||||||
|
* Events sent between client and server
|
||||||
|
* Configuration API
|
||||||
|
* Logging API based on `java.util.logging`
|
||||||
|
* Envoy-specific Exception
|
||||||
|
* Useful utility classes
|
||||||
|
|
||||||
|
### Server
|
||||||
|
* Non-blocking connectivity infrastructure based on `java.nio`
|
||||||
|
* Processors to handle incoming events
|
||||||
|
* Database connectivity
|
||||||
|
* Databse entities
|
||||||
|
* Utility classes to check client version compatability and password validity
|
@ -13,9 +13,9 @@
|
|||||||
</classpathentry>
|
</classpathentry>
|
||||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||||
<attributes>
|
<attributes>
|
||||||
|
<attribute name="test" value="true"/>
|
||||||
<attribute name="optional" value="true"/>
|
<attribute name="optional" value="true"/>
|
||||||
<attribute name="maven.pomderived" value="true"/>
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
<attribute name="test" value="true"/>
|
|
||||||
</attributes>
|
</attributes>
|
||||||
</classpathentry>
|
</classpathentry>
|
||||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||||
|
4
client/.gitignore
vendored
4
client/.gitignore
vendored
@ -1,4 +0,0 @@
|
|||||||
/target/
|
|
||||||
|
|
||||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
|
||||||
hs_err_pid*
|
|
@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE xml>
|
||||||
<projectDescription>
|
<projectDescription>
|
||||||
<name>envoy-client</name>
|
<name>client</name>
|
||||||
<comment></comment>
|
<comment></comment>
|
||||||
<projects>
|
<projects>
|
||||||
</projects>
|
</projects>
|
||||||
|
@ -18,6 +18,7 @@ org.eclipse.jdt.core.compiler.debug.localVariable=generate
|
|||||||
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
|
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
|
||||||
org.eclipse.jdt.core.compiler.doc.comment.support=enabled
|
org.eclipse.jdt.core.compiler.doc.comment.support=enabled
|
||||||
org.eclipse.jdt.core.compiler.problem.APILeak=warning
|
org.eclipse.jdt.core.compiler.problem.APILeak=warning
|
||||||
|
org.eclipse.jdt.core.compiler.problem.annotatedTypeArgumentToUnannotated=info
|
||||||
org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
|
org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
|
||||||
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
||||||
org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
|
org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,3 +0,0 @@
|
|||||||
default.configuration=
|
|
||||||
eclipse.preferences.version=1
|
|
||||||
hibernate3.enabled=false
|
|
@ -9,14 +9,14 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>informatik-ag-ngl</groupId>
|
<groupId>informatik-ag-ngl</groupId>
|
||||||
<artifactId>envoy</artifactId>
|
<artifactId>envoy</artifactId>
|
||||||
<version>0.1-beta</version>
|
<version>0.2-beta</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>informatik-ag-ngl</groupId>
|
<groupId>informatik-ag-ngl</groupId>
|
||||||
<artifactId>envoy-common</artifactId>
|
<artifactId>envoy-common</artifactId>
|
||||||
<version>0.1-beta</version>
|
<version>0.2-beta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
@ -37,15 +37,6 @@
|
|||||||
<directory>src/main/resources</directory>
|
<directory>src/main/resources</directory>
|
||||||
</resource>
|
</resource>
|
||||||
</resources>
|
</resources>
|
||||||
<pluginManagement>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
|
||||||
<version>3.8.1</version>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</pluginManagement>
|
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
@ -9,15 +9,18 @@ import envoy.client.ui.Startup;
|
|||||||
* <p>
|
* <p>
|
||||||
* To allow Maven shading, the main method has to be separated from the
|
* To allow Maven shading, the main method has to be separated from the
|
||||||
* {@link Startup} class which extends {@link Application}.
|
* {@link Startup} class which extends {@link Application}.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>Main.java</strong><br>
|
|
||||||
* Created: <strong>05.07.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public class Main {
|
public final class Main {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A funny debug switch put in by {@code delvh} to enable easy debugging.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
private static final boolean debug = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the application.
|
* Starts the application.
|
||||||
@ -26,5 +29,12 @@ public class Main {
|
|||||||
* client configuration
|
* client configuration
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public static void main(String[] args) { Application.launch(Startup.class, args); }
|
public static void main(String[] args) {
|
||||||
|
if (debug) {
|
||||||
|
// Put testing code here
|
||||||
|
System.out.println();
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
Application.launch(Startup.class, args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,14 @@
|
|||||||
package envoy.client.data;
|
package envoy.client.data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.LinkedList;
|
import java.util.*;
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.*;
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import envoy.util.EnvoyLog;
|
import envoy.util.EnvoyLog;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores elements in a queue to process them later.
|
* Stores elements in a queue to process them later.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>Cache.java</strong><br>
|
|
||||||
* Created: <strong>6 Feb 2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @param <T> the type of cached elements
|
* @param <T> the type of cached elements
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
@ -62,4 +56,11 @@ public final class Cache<T> implements Consumer<T>, Serializable {
|
|||||||
elements.forEach(processor::accept);
|
elements.forEach(processor::accept);
|
||||||
elements.clear();
|
elements.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears this cache of all stored elements.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public void clear() { elements.clear(); }
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
package envoy.client.data;
|
package envoy.client.data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores a heterogeneous map of {@link Cache} objects with different type
|
* Stores a heterogeneous map of {@link Cache} objects with different type
|
||||||
* parameters.
|
* parameters.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>CacheMap.java</strong><br>
|
|
||||||
* Created: <strong>09.07.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
@ -52,7 +47,7 @@ public final class CacheMap implements Serializable {
|
|||||||
public <T> Cache<? super T> getApplicable(Class<T> key) {
|
public <T> Cache<? super T> getApplicable(Class<T> key) {
|
||||||
Cache<? super T> cache = get(key);
|
Cache<? super T> cache = get(key);
|
||||||
if (cache == null)
|
if (cache == null)
|
||||||
for (var e : map.entrySet())
|
for (final var e : map.entrySet())
|
||||||
if (e.getKey().isAssignableFrom(key))
|
if (e.getKey().isAssignableFrom(key))
|
||||||
cache = (Cache<? super T>) e.getValue();
|
cache = (Cache<? super T>) e.getValue();
|
||||||
return cache;
|
return cache;
|
||||||
@ -63,4 +58,11 @@ public final class CacheMap implements Serializable {
|
|||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public Map<Class<?>, Cache<?>> getMap() { return map; }
|
public Map<Class<?>, Cache<?>> getMap() { return map; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the caches of this map of any values.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public void clear() { map.values().forEach(Cache::clear); }
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
package envoy.client.data;
|
package envoy.client.data;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.*;
|
||||||
import java.io.Serializable;
|
import java.util.*;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import javafx.collections.*;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import envoy.client.net.WriteProxy;
|
import envoy.client.net.WriteProxy;
|
||||||
import envoy.data.*;
|
import envoy.data.*;
|
||||||
@ -14,10 +13,6 @@ import envoy.event.MessageStatusChange;
|
|||||||
/**
|
/**
|
||||||
* Represents a chat between two {@link User}s
|
* Represents a chat between two {@link User}s
|
||||||
* as a list of {@link Message} objects.
|
* as a list of {@link Message} objects.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>Chat.java</strong><br>
|
|
||||||
* Created: <strong>19 Oct 2019</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Maximilian Käfer
|
* @author Maximilian Käfer
|
||||||
* @author Leon Hofmeister
|
* @author Leon Hofmeister
|
||||||
@ -26,12 +21,18 @@ import envoy.event.MessageStatusChange;
|
|||||||
*/
|
*/
|
||||||
public class Chat implements Serializable {
|
public class Chat implements Serializable {
|
||||||
|
|
||||||
protected final Contact recipient;
|
protected final Contact recipient;
|
||||||
protected final List<Message> messages = new ArrayList<>();
|
|
||||||
|
protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
|
||||||
|
|
||||||
protected int unreadAmount;
|
protected int unreadAmount;
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
/**
|
||||||
|
* Stores the last time an {@link envoy.event.IsTyping} event has been sent.
|
||||||
|
*/
|
||||||
|
protected transient long lastWritingEvent;
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 2L;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the list of messages that the recipient receives.
|
* Provides the list of messages that the recipient receives.
|
||||||
@ -41,12 +42,20 @@ public class Chat implements Serializable {
|
|||||||
* @param recipient the user who receives the messages
|
* @param recipient the user who receives the messages
|
||||||
* @since Envoy Client v0.1-alpha
|
* @since Envoy Client v0.1-alpha
|
||||||
*/
|
*/
|
||||||
public Chat(Contact recipient) {
|
public Chat(Contact recipient) { this.recipient = recipient; }
|
||||||
this.recipient = recipient;
|
|
||||||
|
private void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException {
|
||||||
|
stream.defaultReadObject();
|
||||||
|
messages = FXCollections.observableList((List<Message>) stream.readObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeObject(ObjectOutputStream stream) throws IOException {
|
||||||
|
stream.defaultWriteObject();
|
||||||
|
stream.writeObject(new ArrayList<>(messages));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() { return String.format("Chat[recipient=%s,messages=%d]", recipient, messages.size()); }
|
public String toString() { return String.format("%s[recipient=%s,messages=%d]", getClass().getSimpleName(), recipient, messages.size()); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a hash code based on the recipient.
|
* Generates a hash code based on the recipient.
|
||||||
@ -65,7 +74,7 @@ public class Chat implements Serializable {
|
|||||||
public boolean equals(Object obj) {
|
public boolean equals(Object obj) {
|
||||||
if (this == obj) return true;
|
if (this == obj) return true;
|
||||||
if (!(obj instanceof Chat)) return false;
|
if (!(obj instanceof Chat)) return false;
|
||||||
Chat other = (Chat) obj;
|
final Chat other = (Chat) obj;
|
||||||
return Objects.equals(recipient, other.recipient);
|
return Objects.equals(recipient, other.recipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,11 +85,9 @@ public class Chat implements Serializable {
|
|||||||
*
|
*
|
||||||
* @param writeProxy the write proxy instance used to notify the server about
|
* @param writeProxy the write proxy instance used to notify the server about
|
||||||
* the message status changes
|
* the message status changes
|
||||||
* @throws IOException if a {@link MessageStatusChange} could not be
|
|
||||||
* delivered to the server
|
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public void read(WriteProxy writeProxy) throws IOException {
|
public void read(WriteProxy writeProxy) {
|
||||||
for (int i = messages.size() - 1; i >= 0; --i) {
|
for (int i = messages.size() - 1; i >= 0; --i) {
|
||||||
final Message m = messages.get(i);
|
final Message m = messages.get(i);
|
||||||
if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break;
|
if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break;
|
||||||
@ -122,7 +129,7 @@ public class Chat implements Serializable {
|
|||||||
public void incrementUnreadAmount() { unreadAmount++; }
|
public void incrementUnreadAmount() { unreadAmount++; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the amount of unread mesages in this chat
|
* @return the amount of unread messages in this chat
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public int getUnreadAmount() { return unreadAmount; }
|
public int getUnreadAmount() { return unreadAmount; }
|
||||||
@ -131,7 +138,7 @@ public class Chat implements Serializable {
|
|||||||
* @return all messages in the current chat
|
* @return all messages in the current chat
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public List<Message> getMessages() { return messages; }
|
public ObservableList<Message> getMessages() { return messages; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the recipient of a message
|
* @return the recipient of a message
|
||||||
@ -140,14 +147,16 @@ public class Chat implements Serializable {
|
|||||||
public Contact getRecipient() { return recipient; }
|
public Contact getRecipient() { return recipient; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return whether this {@link Chat} points at a {@link User}
|
* @return the last known time a {@link envoy.event.IsTyping} event has been
|
||||||
* @since Envoy Client v0.1-beta
|
* sent
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
*/
|
*/
|
||||||
public boolean isUserChat() { return recipient instanceof User; }
|
public long getLastWritingEvent() { return lastWritingEvent; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return whether this {@link Chat} points at a {@link Group}
|
* Sets the {@code lastWritingEvent} to {@code System#currentTimeMillis()}.
|
||||||
* @since Envoy Client v0.1-beta
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
*/
|
*/
|
||||||
public boolean isGroupChat() { return recipient instanceof Group; }
|
public void lastWritingEventWasNow() { lastWritingEvent = System.currentTimeMillis(); }
|
||||||
}
|
}
|
||||||
|
@ -2,26 +2,16 @@ package envoy.client.data;
|
|||||||
|
|
||||||
import static java.util.function.Function.identity;
|
import static java.util.function.Function.identity;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
|
|
||||||
import envoy.client.ui.Startup;
|
|
||||||
import envoy.data.Config;
|
import envoy.data.Config;
|
||||||
import envoy.data.ConfigItem;
|
|
||||||
import envoy.data.LoginCredentials;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a configuration specific to the Envoy Client with default values
|
* Implements a configuration specific to the Envoy Client with default values
|
||||||
* and convenience methods.
|
* and convenience methods.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>ClientConfig.java</strong><br>
|
|
||||||
* Created: <strong>01.03.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public class ClientConfig extends Config {
|
public final class ClientConfig extends Config {
|
||||||
|
|
||||||
private static ClientConfig config;
|
private static ClientConfig config;
|
||||||
|
|
||||||
@ -35,15 +25,10 @@ public class ClientConfig extends Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ClientConfig() {
|
private ClientConfig() {
|
||||||
items.put("server", new ConfigItem<>("server", "s", identity(), null, true));
|
super(".envoy");
|
||||||
items.put("port", new ConfigItem<>("port", "p", Integer::parseInt, null, true));
|
put("server", "s", identity());
|
||||||
items.put("localDB", new ConfigItem<>("localDB", "db", File::new, new File("localDB"), true));
|
put("port", "p", Integer::parseInt);
|
||||||
items.put("ignoreLocalDB", new ConfigItem<>("ignoreLocalDB", "nodb", Boolean::parseBoolean, false, false));
|
put("localDBSaveInterval", "db-si", Integer::parseInt);
|
||||||
items.put("homeDirectory", new ConfigItem<>("homeDirectory", "h", File::new, new File(System.getProperty("user.home"), ".envoy"), true));
|
|
||||||
items.put("fileLevelBarrier", new ConfigItem<>("fileLevelBarrier", "fb", Level::parse, Level.CONFIG, true));
|
|
||||||
items.put("consoleLevelBarrier", new ConfigItem<>("consoleLevelBarrier", "cb", Level::parse, Level.FINEST, true));
|
|
||||||
items.put("user", new ConfigItem<>("user", "u", identity()));
|
|
||||||
items.put("password", new ConfigItem<>("password", "pw", identity()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -59,57 +44,8 @@ public class ClientConfig extends Config {
|
|||||||
public Integer getPort() { return (Integer) items.get("port").get(); }
|
public Integer getPort() { return (Integer) items.get("port").get(); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the local database specific to the client user
|
* @return the amount of minutes after which the local database should be saved
|
||||||
* @since Envoy Client v0.1-alpha
|
* @since Envoy Client v0.2-beta
|
||||||
*/
|
*/
|
||||||
public File getLocalDB() { return (File) items.get("localDB").get(); }
|
public Integer getLocalDBSaveInterval() { return (Integer) items.get("localDBSaveInterval").get(); }
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {@code true} if the local database is to be ignored
|
|
||||||
* @since Envoy Client v0.3-alpha
|
|
||||||
*/
|
|
||||||
public Boolean isIgnoreLocalDB() { return (Boolean) items.get("ignoreLocalDB").get(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the directory in which all local files are saves
|
|
||||||
* @since Envoy Client v0.2-alpha
|
|
||||||
*/
|
|
||||||
public File getHomeDirectory() { return (File) items.get("homeDirectory").get(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the minimal {@link Level} to log inside the log file
|
|
||||||
* @since Envoy Client v0.2-alpha
|
|
||||||
*/
|
|
||||||
public Level getFileLevelBarrier() { return (Level) items.get("fileLevelBarrier").get(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the minimal {@link Level} to log inside the console
|
|
||||||
* @since Envoy Client v0.2-alpha
|
|
||||||
*/
|
|
||||||
public Level getConsoleLevelBarrier() { return (Level) items.get("consoleLevelBarrier").get(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the user name
|
|
||||||
* @since Envoy Client v0.3-alpha
|
|
||||||
*/
|
|
||||||
public String getUser() { return (String) items.get("user").get(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the password
|
|
||||||
* @since Envoy Client v0.3-alpha
|
|
||||||
*/
|
|
||||||
public String getPassword() { return (String) items.get("password").get(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {@code true} if user name and password are set
|
|
||||||
* @since Envoy Client v0.3-alpha
|
|
||||||
*/
|
|
||||||
public boolean hasLoginCredentials() { return getUser() != null && getPassword() != null; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return login credentials for the specified user name and password, without
|
|
||||||
* the registration option
|
|
||||||
* @since Envoy Client v0.3-alpha
|
|
||||||
*/
|
|
||||||
public LoginCredentials getLoginCredentials() { return new LoginCredentials(getUser(), getPassword(), false, Startup.VERSION); }
|
|
||||||
}
|
}
|
||||||
|
93
client/src/main/java/envoy/client/data/Context.java
Normal file
93
client/src/main/java/envoy/client/data/Context.java
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package envoy.client.data;
|
||||||
|
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
import envoy.client.net.*;
|
||||||
|
import envoy.client.ui.SceneContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides access to commonly used objects.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public class Context {
|
||||||
|
|
||||||
|
private WriteProxy writeProxy;
|
||||||
|
private LocalDB localDB;
|
||||||
|
private Stage stage;
|
||||||
|
private SceneContext sceneContext;
|
||||||
|
|
||||||
|
private final Client client = new Client();
|
||||||
|
|
||||||
|
private static final Context instance = new Context();
|
||||||
|
|
||||||
|
private Context() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the instance of {@code Context} used throughout Envoy
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public static Context getInstance() { return instance; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the write proxy given that {@code localDB} is initialized.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public void initWriteProxy() {
|
||||||
|
if (localDB == null) throw new IllegalStateException("The LocalDB has to be initialized!");
|
||||||
|
writeProxy = new WriteProxy(client, localDB);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the localDB
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public LocalDB getLocalDB() { return localDB; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param localDB the localDB to set
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public void setLocalDB(LocalDB localDB) { this.localDB = localDB; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the sceneContext
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SceneContext getSceneContext() { return sceneContext; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param sceneContext the sceneContext to set. Additionally sets the stage.
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public void setSceneContext(SceneContext sceneContext) {
|
||||||
|
this.sceneContext = sceneContext;
|
||||||
|
stage = sceneContext.getStage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the client
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public Client getClient() { return client; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the writeProxy
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public WriteProxy getWriteProxy() { return writeProxy; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the stage
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public Stage getStage() { return stage; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param stage the stage to set
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public void setStage(Stage stage) { this.stage = stage; }
|
||||||
|
}
|
@ -1,27 +1,20 @@
|
|||||||
package envoy.client.data;
|
package envoy.client.data;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
import envoy.client.net.WriteProxy;
|
import envoy.client.net.WriteProxy;
|
||||||
import envoy.data.Contact;
|
import envoy.data.*;
|
||||||
import envoy.data.GroupMessage;
|
|
||||||
import envoy.data.Message.MessageStatus;
|
import envoy.data.Message.MessageStatus;
|
||||||
import envoy.data.User;
|
|
||||||
import envoy.event.GroupMessageStatusChange;
|
import envoy.event.GroupMessageStatusChange;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a chat between a user and a group
|
* Represents a chat between a user and a group
|
||||||
* as a list of messages.
|
* as a list of messages.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>GroupChat.java</strong><br>
|
|
||||||
* Created: <strong>05.07.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Maximilian Käfer
|
* @author Maximilian Käfer
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public class GroupChat extends Chat {
|
public final class GroupChat extends Chat {
|
||||||
|
|
||||||
private final User sender;
|
private final User sender;
|
||||||
|
|
||||||
@ -38,16 +31,13 @@ public class GroupChat extends Chat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(WriteProxy writeProxy) throws IOException {
|
public void read(WriteProxy writeProxy) {
|
||||||
for (int i = messages.size() - 1; i >= 0; --i) {
|
for (int i = messages.size() - 1; i >= 0; --i) {
|
||||||
final GroupMessage gmsg = (GroupMessage) messages.get(i);
|
final GroupMessage gmsg = (GroupMessage) messages.get(i);
|
||||||
if (gmsg.getSenderID() != sender.getID()) {
|
if (gmsg.getSenderID() != sender.getID()) if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ) break;
|
||||||
if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ) break;
|
else {
|
||||||
else {
|
gmsg.getMemberStatuses().replace(sender.getID(), MessageStatus.READ);
|
||||||
gmsg.getMemberStatuses().replace(sender.getID(), MessageStatus.READ);
|
writeProxy.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(), MessageStatus.READ, Instant.now(), sender.getID()));
|
||||||
writeProxy
|
|
||||||
.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(), MessageStatus.READ, LocalDateTime.now(), sender.getID()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unreadAmount = 0;
|
unreadAmount = 0;
|
||||||
|
@ -1,74 +1,147 @@
|
|||||||
package envoy.client.data;
|
package envoy.client.data;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.channels.*;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.logging.*;
|
||||||
|
|
||||||
|
import javafx.collections.*;
|
||||||
|
|
||||||
|
import envoy.client.event.*;
|
||||||
import envoy.data.*;
|
import envoy.data.*;
|
||||||
import envoy.event.GroupResize;
|
import envoy.data.Message.MessageStatus;
|
||||||
import envoy.event.MessageStatusChange;
|
import envoy.event.*;
|
||||||
import envoy.event.NameChange;
|
import envoy.exception.EnvoyException;
|
||||||
|
import envoy.util.*;
|
||||||
|
|
||||||
|
import dev.kske.eventbus.Event;
|
||||||
|
import dev.kske.eventbus.EventBus;
|
||||||
|
import dev.kske.eventbus.EventListener;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores information about the current {@link User} and their {@link Chat}s.
|
* Stores information about the current {@link User} and their {@link Chat}s.
|
||||||
* For message ID generation a {@link IDGenerator} is stored as well.
|
* For message ID generation a {@link IDGenerator} is stored as well.
|
||||||
* <p>
|
* <p>
|
||||||
* Project: <strong>envoy-client</strong><br>
|
* The managed objects are stored inside a folder in the local file system.
|
||||||
* File: <strong>LocalDB.java</strong><br>
|
|
||||||
* Created: <strong>3 Feb 2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public abstract class LocalDB {
|
public final class LocalDB implements EventListener {
|
||||||
|
|
||||||
protected User user;
|
// Data
|
||||||
protected Map<String, Contact> users = new HashMap<>();
|
private User user;
|
||||||
protected List<Chat> chats = new ArrayList<>();
|
private Map<String, User> users = Collections.synchronizedMap(new HashMap<>());
|
||||||
protected IDGenerator idGenerator;
|
private ObservableList<Chat> chats = FXCollections.observableArrayList();
|
||||||
protected CacheMap cacheMap = new CacheMap();
|
private IDGenerator idGenerator;
|
||||||
|
private CacheMap cacheMap = new CacheMap();
|
||||||
|
private String authToken;
|
||||||
|
|
||||||
{
|
// Auto save timer
|
||||||
|
private Timer autoSaver;
|
||||||
|
private boolean autoSaveRestart = true;
|
||||||
|
|
||||||
|
// State management
|
||||||
|
private Instant lastSync = Instant.EPOCH;
|
||||||
|
|
||||||
|
// Persistence
|
||||||
|
private File userFile;
|
||||||
|
private FileLock instanceLock;
|
||||||
|
|
||||||
|
private final File dbDir, idGeneratorFile, lastLoginFile, usersFile;
|
||||||
|
|
||||||
|
private static final Logger logger = EnvoyLog.getLogger(LocalDB.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an empty local database.
|
||||||
|
*
|
||||||
|
* @param dbDir the directory in which to persist data
|
||||||
|
* @throws IOException if {@code dbDir} is a file (and not a directory)
|
||||||
|
* @throws EnvoyException if {@code dbDir} is in use by another Envoy instance
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public LocalDB(File dbDir) throws IOException, EnvoyException {
|
||||||
|
this.dbDir = dbDir;
|
||||||
|
EventBus.getInstance().registerListener(this);
|
||||||
|
|
||||||
|
// Ensure that the database directory exists
|
||||||
|
if (!dbDir.exists()) dbDir.mkdirs();
|
||||||
|
else if (!dbDir.isDirectory()) throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
|
||||||
|
|
||||||
|
// Lock the directory
|
||||||
|
lock();
|
||||||
|
|
||||||
|
// Initialize global files
|
||||||
|
idGeneratorFile = new File(dbDir, "id_gen.db");
|
||||||
|
lastLoginFile = new File(dbDir, "last_login.db");
|
||||||
|
usersFile = new File(dbDir, "users.db");
|
||||||
|
|
||||||
|
// Load global files
|
||||||
|
loadGlobalData();
|
||||||
|
|
||||||
|
// Initialize offline caches
|
||||||
cacheMap.put(Message.class, new Cache<>());
|
cacheMap.put(Message.class, new Cache<>());
|
||||||
cacheMap.put(MessageStatusChange.class, new Cache<>());
|
cacheMap.put(MessageStatusChange.class, new Cache<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a storage space for a user-specific list of chats.
|
* Ensured that only one Envoy instance is using this local database by creating
|
||||||
|
* a lock file.
|
||||||
|
* The lock file is deleted on application exit.
|
||||||
*
|
*
|
||||||
* @since Envoy Client v0.3-alpha
|
* @throws EnvoyException if the lock cannot by acquired
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
*/
|
*/
|
||||||
public void initializeUserStorage() {}
|
private synchronized void lock() throws EnvoyException {
|
||||||
|
final var file = new File(dbDir, "instance.lock");
|
||||||
|
try {
|
||||||
|
final var fc = FileChannel.open(file.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
|
||||||
|
instanceLock = fc.tryLock();
|
||||||
|
if (instanceLock == null) throw new EnvoyException("Another Envoy instance is using this local database!");
|
||||||
|
} catch (final IOException e) {
|
||||||
|
throw new EnvoyException("Could not create lock file!", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores all users. If the client user is specified, their chats will be stored
|
* Loads the local user registry {@code users.db}, the id generator
|
||||||
* as well. The message id generator will also be saved if present.
|
* {@code id_gen.db} and last login file {@code last_login.db}.
|
||||||
*
|
*
|
||||||
* @throws Exception if the saving process failed
|
* @since Envoy Client v0.2-beta
|
||||||
* @since Envoy Client v0.3-alpha
|
|
||||||
*/
|
*/
|
||||||
public void save() throws Exception {}
|
private synchronized void loadGlobalData() {
|
||||||
|
try {
|
||||||
/**
|
try (var in = new ObjectInputStream(new FileInputStream(usersFile))) {
|
||||||
* Loads all user data.
|
users = (Map<String, User>) in.readObject();
|
||||||
*
|
}
|
||||||
* @throws Exception if the loading process failed
|
idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
|
||||||
* @since Envoy Client v0.3-alpha
|
try (var in = new ObjectInputStream(new FileInputStream(lastLoginFile))) {
|
||||||
*/
|
user = (User) in.readObject();
|
||||||
public void loadUsers() throws Exception {}
|
authToken = (String) in.readObject();
|
||||||
|
}
|
||||||
|
} catch (IOException | ClassNotFoundException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads all data of the client user.
|
* Loads all data of the client user.
|
||||||
*
|
*
|
||||||
* @throws Exception if the loading process failed
|
* @throws ClassNotFoundException if the loading process failed
|
||||||
|
* @throws IOException if the loading process failed
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public void loadUserData() throws Exception {}
|
public synchronized void loadUserData() throws ClassNotFoundException, IOException {
|
||||||
|
if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage");
|
||||||
/**
|
userFile = new File(dbDir, user.getID() + ".db");
|
||||||
* Loads the ID generator. Any exception thrown during this process is ignored.
|
try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
|
||||||
*
|
chats = FXCollections.observableList((List<Chat>) in.readObject());
|
||||||
* @since Envoy Client v0.3-alpha
|
cacheMap = (CacheMap) in.readObject();
|
||||||
*/
|
lastSync = (Instant) in.readObject();
|
||||||
public void loadIDGenerator() {}
|
} finally {
|
||||||
|
synchronize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronizes the contact list of the client user with the chat and user
|
* Synchronizes the contact list of the client user with the chat and user
|
||||||
@ -76,14 +149,13 @@ public abstract class LocalDB {
|
|||||||
*
|
*
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public void synchronize() {
|
private void synchronize() {
|
||||||
user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), u));
|
user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), (User) u));
|
||||||
users.put(user.getName(), user);
|
users.put(user.getName(), user);
|
||||||
|
|
||||||
// Synchronize user status data
|
// Synchronize user status data
|
||||||
for (Contact contact : users.values())
|
for (final var contact : users.values())
|
||||||
if (contact instanceof User)
|
if (contact instanceof User) getChat(contact.getID()).ifPresent(chat -> { ((User) chat.getRecipient()).setStatus(contact.getStatus()); });
|
||||||
getChat(contact.getID()).ifPresent(chat -> { ((User) chat.getRecipient()).setStatus(((User) contact).getStatus()); });
|
|
||||||
|
|
||||||
// Create missing chats
|
// Create missing chats
|
||||||
user.getContacts()
|
user.getContacts()
|
||||||
@ -93,24 +165,147 @@ public abstract class LocalDB {
|
|||||||
.forEach(chats::add);
|
.forEach(chats::add);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a timer that automatically saves this local database after a
|
||||||
|
* period of time specified in the settings.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public void initAutoSave() {
|
||||||
|
|
||||||
|
// A logout happened so the timer should be restarted
|
||||||
|
if (autoSaveRestart) {
|
||||||
|
autoSaver = new Timer("LocalDB Autosave", true);
|
||||||
|
autoSaveRestart = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
autoSaver.schedule(new TimerTask() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() { save(); }
|
||||||
|
}, 2000, ClientConfig.getInstance().getLocalDBSaveInterval() * 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores all users. If the client user is specified, their chats will be stored
|
||||||
|
* as well. The message id generator will also be saved if present.
|
||||||
|
*
|
||||||
|
* @throws IOException if the saving process failed
|
||||||
|
* @since Envoy Client v0.3-alpha
|
||||||
|
*/
|
||||||
|
@Event(eventType = EnvoyCloseEvent.class, priority = 1000)
|
||||||
|
private synchronized void save() {
|
||||||
|
EnvoyLog.getLogger(LocalDB.class).log(Level.INFO, "Saving local database...");
|
||||||
|
|
||||||
|
// Save users
|
||||||
|
try {
|
||||||
|
SerializationUtils.write(usersFile, users);
|
||||||
|
|
||||||
|
// Save user data and last sync time stamp
|
||||||
|
if (user != null) SerializationUtils
|
||||||
|
.write(userFile, new ArrayList<>(chats), cacheMap, Context.getInstance().getClient().isOnline() ? Instant.now() : lastSync);
|
||||||
|
|
||||||
|
// Save last login information
|
||||||
|
if (authToken != null) SerializationUtils.write(lastLoginFile, user, authToken);
|
||||||
|
|
||||||
|
// Save ID generator
|
||||||
|
if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
EnvoyLog.getLogger(LocalDB.class).log(Level.SEVERE, "Unable to save local database: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Event(priority = 150)
|
||||||
|
private void onMessage(Message msg) { if (msg.getStatus() == MessageStatus.SENT) msg.nextStatus(); }
|
||||||
|
|
||||||
|
@Event(priority = 150)
|
||||||
|
private void onGroupMessage(GroupMessage msg) {
|
||||||
|
// TODO: Cancel event once EventBus is updated
|
||||||
|
if (msg.getStatus() == MessageStatus.WAITING || msg.getStatus() == MessageStatus.READ)
|
||||||
|
logger.warning("The groupMessage has the unexpected status " + msg.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Event(priority = 150)
|
||||||
|
private void onMessageStatusChange(MessageStatusChange evt) { getMessage(evt.getID()).ifPresent(msg -> msg.setStatus(evt.get())); }
|
||||||
|
|
||||||
|
@Event(priority = 150)
|
||||||
|
private void onGroupMessageStatusChange(GroupMessageStatusChange evt) {
|
||||||
|
this.<GroupMessage>getMessage(evt.getID()).ifPresent(msg -> msg.getMemberStatuses().replace(evt.getMemberID(), evt.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Event(priority = 150)
|
||||||
|
private void onUserStatusChange(UserStatusChange evt) {
|
||||||
|
this.getChat(evt.getID()).map(Chat::getRecipient).map(User.class::cast).ifPresent(u -> u.setStatus(evt.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Event(priority = 150)
|
||||||
|
private void onGroupResize(GroupResize evt) { getChat(evt.getGroupID()).map(Chat::getRecipient).map(Group.class::cast).ifPresent(evt::apply); }
|
||||||
|
|
||||||
|
@Event(priority = 150)
|
||||||
|
private void onNameChange(NameChange evt) {
|
||||||
|
chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == evt.getID()).findAny().ifPresent(c -> c.setName(evt.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores a new authentication token.
|
||||||
|
*
|
||||||
|
* @param evt the event containing the authentication token
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
@Event
|
||||||
|
private void onNewAuthToken(NewAuthToken evt) { authToken = evt.get(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all associations to the current user.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
@Event(eventType = Logout.class, priority = 100)
|
||||||
|
private void onLogout() {
|
||||||
|
autoSaver.cancel();
|
||||||
|
autoSaveRestart = true;
|
||||||
|
lastLoginFile.delete();
|
||||||
|
userFile = null;
|
||||||
|
user = null;
|
||||||
|
authToken = null;
|
||||||
|
chats.clear();
|
||||||
|
lastSync = Instant.EPOCH;
|
||||||
|
cacheMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return a {@code Map<String, User>} of all users stored locally with their
|
* @return a {@code Map<String, User>} of all users stored locally with their
|
||||||
* user names as keys
|
* user names as keys
|
||||||
* @since Envoy Client v0.2-alpha
|
* @since Envoy Client v0.2-alpha
|
||||||
*/
|
*/
|
||||||
public Map<String, Contact> getUsers() { return users; }
|
public Map<String, User> getUsers() { return users; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for a message by ID.
|
||||||
|
*
|
||||||
|
* @param id the ID of the message to search for
|
||||||
|
* @return an optional containing the message
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public <T extends Message> Optional<T> getMessage(long id) {
|
||||||
|
return (Optional<T>) chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for a chat by recipient ID.
|
||||||
|
*
|
||||||
|
* @param recipientID the ID of the chat's recipient
|
||||||
|
* @return an optional containing the chat
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public Optional<Chat> getChat(long recipientID) { return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny(); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return all saved {@link Chat} objects that list the client user as the
|
* @return all saved {@link Chat} objects that list the client user as the
|
||||||
* sender
|
* sender
|
||||||
* @since Envoy Client v0.1-alpha
|
* @since Envoy Client v0.1-alpha
|
||||||
**/
|
**/
|
||||||
public List<Chat> getChats() { return chats; }
|
public ObservableList<Chat> getChats() { return chats; }
|
||||||
|
|
||||||
/**
|
|
||||||
* @param chats the chats to set
|
|
||||||
*/
|
|
||||||
public void setChats(List<Chat> chats) { this.chats = chats; }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the {@link User} who initialized the local database
|
* @return the {@link User} who initialized the local database
|
||||||
@ -134,6 +329,7 @@ public abstract class LocalDB {
|
|||||||
* @param idGenerator the message ID generator to set
|
* @param idGenerator the message ID generator to set
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
|
@Event(priority = 150)
|
||||||
public void setIDGenerator(IDGenerator idGenerator) { this.idGenerator = idGenerator; }
|
public void setIDGenerator(IDGenerator idGenerator) { this.idGenerator = idGenerator; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -149,57 +345,14 @@ public abstract class LocalDB {
|
|||||||
public CacheMap getCacheMap() { return cacheMap; }
|
public CacheMap getCacheMap() { return cacheMap; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches for a message by ID.
|
* @return the time stamp when the database was last saved
|
||||||
*
|
* @since Envoy Client v0.2-beta
|
||||||
* @param id the ID of the message to search for
|
|
||||||
* @return an optional containing the message
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
*/
|
||||||
public Optional<Message> getMessage(long id) {
|
public Instant getLastSync() { return lastSync; }
|
||||||
return chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches for a chat by recipient ID.
|
* @return the authentication token of the user
|
||||||
*
|
* @since Envoy Client v0.2-beta
|
||||||
* @param recipientID the ID of the chat's recipient
|
|
||||||
* @return an optional containing the chat
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
*/
|
||||||
public Optional<Chat> getChat(long recipientID) { return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny(); }
|
public String getAuthToken() { return authToken; }
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a contact name change if the corresponding contact is present.
|
|
||||||
*
|
|
||||||
* @param event the {@link NameChange} to process
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public void replaceContactName(NameChange event) {
|
|
||||||
chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == event.getID()).findAny().ifPresent(c -> c.setName(event.get()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a group resize operation if the corresponding group is present.
|
|
||||||
*
|
|
||||||
* @param event the {@link GroupResize} to process
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public void updateGroup(GroupResize event) {
|
|
||||||
chats.stream()
|
|
||||||
.map(Chat::getRecipient)
|
|
||||||
.filter(Group.class::isInstance)
|
|
||||||
.filter(g -> g.getID() == event.getGroupID() && g.getID() != user.getID())
|
|
||||||
.map(Group.class::cast)
|
|
||||||
.findAny()
|
|
||||||
.ifPresent(group -> {
|
|
||||||
switch (event.getOperation()) {
|
|
||||||
case ADD:
|
|
||||||
group.getContacts().add(event.get());
|
|
||||||
break;
|
|
||||||
case REMOVE:
|
|
||||||
group.getContacts().remove(event.get());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,88 +0,0 @@
|
|||||||
package envoy.client.data;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
import envoy.data.IDGenerator;
|
|
||||||
import envoy.util.SerializationUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements a {@link LocalDB} in a way that stores all information inside a
|
|
||||||
* folder on the local file system.
|
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>PersistentLocalDB.java</strong><br>
|
|
||||||
* Created: <strong>27.10.2019</strong><br>
|
|
||||||
*
|
|
||||||
* @author Kai S. K. Engelbart
|
|
||||||
* @author Maximilian Käfer
|
|
||||||
* @since Envoy Client v0.1-alpha
|
|
||||||
*/
|
|
||||||
public final class PersistentLocalDB extends LocalDB {
|
|
||||||
|
|
||||||
private File dbDir, userFile, idGeneratorFile, usersFile;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs an empty local database. To serialize any user-specific data to
|
|
||||||
* the file system, call {@link PersistentLocalDB#initializeUserStorage()} first
|
|
||||||
* and then {@link PersistentLocalDB#save()}.
|
|
||||||
*
|
|
||||||
* @param dbDir the directory in which to persist data
|
|
||||||
* @throws IOException if {@code dbDir} is a file (and not a directory)
|
|
||||||
* @since Envoy Client v0.1-alpha
|
|
||||||
*/
|
|
||||||
public PersistentLocalDB(File dbDir) throws IOException {
|
|
||||||
this.dbDir = dbDir;
|
|
||||||
|
|
||||||
// Test if the database directory is actually a directory
|
|
||||||
if (dbDir.exists() && !dbDir.isDirectory())
|
|
||||||
throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
|
|
||||||
|
|
||||||
// Initialize global files
|
|
||||||
idGeneratorFile = new File(dbDir, "id_gen.db");
|
|
||||||
usersFile = new File(dbDir, "users.db");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a database file for a user-specific list of chats.
|
|
||||||
*
|
|
||||||
* @throws IllegalStateException if the client user is not specified
|
|
||||||
* @since Envoy Client v0.1-alpha
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void initializeUserStorage() {
|
|
||||||
if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage");
|
|
||||||
userFile = new File(dbDir, user.getID() + ".db");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void save() throws IOException {
|
|
||||||
// Save users
|
|
||||||
SerializationUtils.write(usersFile, users);
|
|
||||||
|
|
||||||
// Save user data
|
|
||||||
if (user != null) SerializationUtils.write(userFile, chats, cacheMap);
|
|
||||||
|
|
||||||
// Save id generator
|
|
||||||
if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadUsers() throws ClassNotFoundException, IOException { users = SerializationUtils.read(usersFile, HashMap.class); }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadUserData() throws ClassNotFoundException, IOException {
|
|
||||||
try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
|
|
||||||
chats = (ArrayList<Chat>) in.readObject();
|
|
||||||
cacheMap = (CacheMap) in.readObject();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadIDGenerator() {
|
|
||||||
try {
|
|
||||||
idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
|
|
||||||
} catch (ClassNotFoundException | IOException e) {}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +1,27 @@
|
|||||||
package envoy.client.data;
|
package envoy.client.data;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.*;
|
||||||
import java.io.IOException;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
import java.util.logging.Level;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.prefs.Preferences;
|
import java.util.prefs.Preferences;
|
||||||
|
|
||||||
import envoy.util.SerializationUtils;
|
import envoy.client.event.EnvoyCloseEvent;
|
||||||
|
import envoy.util.*;
|
||||||
|
|
||||||
|
import dev.kske.eventbus.*;
|
||||||
|
import dev.kske.eventbus.EventListener;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages all application settings, which are different objects that can be
|
* Manages all application settings, which are different objects that can be
|
||||||
* changed during runtime and serialized them by using either the file system or
|
* changed during runtime and serialized them by using either the file system or
|
||||||
* the {@link Preferences} API.
|
* the {@link Preferences} API.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>Settings.java</strong><br>
|
|
||||||
* Created: <strong>11 Nov 2019</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Leon Hofmeister
|
* @author Leon Hofmeister
|
||||||
* @author Maximilian Käfer
|
* @author Maximilian Käfer
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.2-alpha
|
* @since Envoy Client v0.2-alpha
|
||||||
*/
|
*/
|
||||||
public class Settings {
|
public final class Settings implements EventListener {
|
||||||
|
|
||||||
// Actual settings accessible by the rest of the application
|
// Actual settings accessible by the rest of the application
|
||||||
private Map<String, SettingsItem<?>> items;
|
private Map<String, SettingsItem<?>> items;
|
||||||
@ -44,6 +43,8 @@ public class Settings {
|
|||||||
* @since Envoy Client v0.2-alpha
|
* @since Envoy Client v0.2-alpha
|
||||||
*/
|
*/
|
||||||
private Settings() {
|
private Settings() {
|
||||||
|
EventBus.getInstance().registerListener(this);
|
||||||
|
|
||||||
// Load settings from settings file
|
// Load settings from settings file
|
||||||
try {
|
try {
|
||||||
items = SerializationUtils.read(settingsFile, HashMap.class);
|
items = SerializationUtils.read(settingsFile, HashMap.class);
|
||||||
@ -67,16 +68,28 @@ public class Settings {
|
|||||||
* @throws IOException if an error occurs while saving the themes
|
* @throws IOException if an error occurs while saving the themes
|
||||||
* @since Envoy Client v0.2-alpha
|
* @since Envoy Client v0.2-alpha
|
||||||
*/
|
*/
|
||||||
public void save() throws IOException {
|
@Event(eventType = EnvoyCloseEvent.class, priority = 900)
|
||||||
|
private void save() {
|
||||||
|
EnvoyLog.getLogger(Settings.class).log(Level.INFO, "Saving settings...");
|
||||||
|
|
||||||
// Save settings to settings file
|
// Save settings to settings file
|
||||||
SerializationUtils.write(settingsFile, items);
|
try {
|
||||||
|
SerializationUtils.write(settingsFile, items);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
EnvoyLog.getLogger(Settings.class).log(Level.SEVERE, "Unable to save settings file: ", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void supplementDefaults() {
|
private void supplementDefaults() {
|
||||||
items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send", "Sends a message by pressing the enter key."));
|
items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send", "Sends a message by pressing the enter key."));
|
||||||
items.putIfAbsent("onCloseMode", new SettingsItem<>(true, "Hide on close", "Hides the chat window when it is closed."));
|
items.putIfAbsent("hideOnClose", new SettingsItem<>(false, "Hide on close", "Hides the chat window when it is closed."));
|
||||||
items.putIfAbsent("currentTheme", new SettingsItem<>("dark", "Current Theme Name", "The name of the currently selected theme."));
|
items.putIfAbsent("currentTheme", new SettingsItem<>("dark", "Current Theme Name", "The name of the currently selected theme."));
|
||||||
|
items.putIfAbsent("downloadLocation",
|
||||||
|
new SettingsItem<>(new File(System.getProperty("user.home") + "/Downloads/"), "Download location",
|
||||||
|
"The location where files will be saved to"));
|
||||||
|
items.putIfAbsent("autoSaveDownloads", new SettingsItem<>(false, "Save without asking?", "Should downloads be saved without asking?"));
|
||||||
|
items.putIfAbsent("askForConfirmation",
|
||||||
|
new SettingsItem<>(true, "Ask for confirmation", "Will ask for confirmation before doing certain things"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -120,19 +133,71 @@ public class Settings {
|
|||||||
*/
|
*/
|
||||||
public void setEnterToSend(boolean enterToSend) { ((SettingsItem<Boolean>) items.get("enterToSend")).set(enterToSend); }
|
public void setEnterToSend(boolean enterToSend) { ((SettingsItem<Boolean>) items.get("enterToSend")).set(enterToSend); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return whether Envoy will prompt a dialogue before saving an
|
||||||
|
* {@link envoy.data.Attachment}
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public Boolean isDownloadSavedWithoutAsking() { return (Boolean) items.get("autoSaveDownloads").get(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether Envoy will prompt a dialogue before saving an
|
||||||
|
* {@link envoy.data.Attachment}.
|
||||||
|
*
|
||||||
|
* @param autosaveDownload whether a download should be saved without asking
|
||||||
|
* before
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public void setDownloadSavedWithoutAsking(boolean autosaveDownload) {
|
||||||
|
((SettingsItem<Boolean>) items.get("autoSaveDownloads")).set(autosaveDownload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the path where downloads should be saved
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public File getDownloadLocation() { return (File) items.get("downloadLocation").get(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the path where downloads should be saved.
|
||||||
|
*
|
||||||
|
* @param downloadLocation the path to set
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public void setDownloadLocation(File downloadLocation) { ((SettingsItem<File>) items.get("downloadLocation")).set(downloadLocation); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the current on close mode.
|
* @return the current on close mode.
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public Boolean getCurrentOnCloseMode() { return (Boolean) items.get("onCloseMode").get(); }
|
public Boolean isHideOnClose() { return (Boolean) items.get("hideOnClose").get(); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the current on close mode.
|
* Sets the current on close mode.
|
||||||
*
|
*
|
||||||
* @param currentOnCloseMode the on close mode that should be set.
|
* @param hideOnClose whether the application should be minimized on close
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public void setCurrentOnCloseMode(boolean currentOnCloseMode) { ((SettingsItem<Boolean>) items.get("onCloseMode")).set(currentOnCloseMode); }
|
public void setHideOnClose(boolean hideOnClose) { ((SettingsItem<Boolean>) items.get("hideOnClose")).set(hideOnClose); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return whether a confirmation dialog should be displayed before certain
|
||||||
|
* actions
|
||||||
|
* @since Envoy Client v0.2-alpha
|
||||||
|
*/
|
||||||
|
public Boolean isAskForConfirmation() { return (Boolean) items.get("askForConfirmation").get(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the behavior of calling certain functionality by displaying a
|
||||||
|
* confirmation dialog before executing it.
|
||||||
|
*
|
||||||
|
* @param askForConfirmation whether confirmation dialogs should be displayed
|
||||||
|
* before certain actions
|
||||||
|
* @since Envoy Client v0.2-alpha
|
||||||
|
*/
|
||||||
|
public void setAskForConfirmation(boolean askForConfirmation) {
|
||||||
|
((SettingsItem<Boolean>) items.get("askForConfirmation")).set(askForConfirmation);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the items
|
* @return the items
|
||||||
|
@ -8,16 +8,12 @@ import javax.swing.JComponent;
|
|||||||
/**
|
/**
|
||||||
* Encapsulates a persistent value that is directly or indirectly mutable by the
|
* Encapsulates a persistent value that is directly or indirectly mutable by the
|
||||||
* user.
|
* user.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>SettingsItem.java</strong><br>
|
|
||||||
* Created: <strong>23.12.2019</strong><br>
|
|
||||||
*
|
*
|
||||||
* @param <T> the type of this {@link SettingsItem}'s value
|
* @param <T> the type of this {@link SettingsItem}'s value
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public class SettingsItem<T> implements Serializable {
|
public final class SettingsItem<T> implements Serializable {
|
||||||
|
|
||||||
private T value;
|
private T value;
|
||||||
private String userFriendlyName, description;
|
private String userFriendlyName, description;
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
package envoy.client.data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements a {@link LocalDB} in a way that does not persist any information
|
|
||||||
* after application shutdown.
|
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>TransientLocalDB.java</strong><br>
|
|
||||||
* Created: <strong>3 Feb 2020</strong><br>
|
|
||||||
*
|
|
||||||
* @author Kai S. K. Engelbart
|
|
||||||
* @since Envoy Client v0.3-alpha
|
|
||||||
*/
|
|
||||||
public final class TransientLocalDB extends LocalDB {
|
|
||||||
}
|
|
@ -6,10 +6,6 @@ import envoy.exception.EnvoyException;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays back audio from a byte array.
|
* Plays back audio from a byte array.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>AudioPlayer.java</strong><br>
|
|
||||||
* Created: <strong>05.07.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
package envoy.client.data.audio;
|
package envoy.client.data.audio;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.*;
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
import javax.sound.sampled.*;
|
import javax.sound.sampled.*;
|
||||||
|
|
||||||
@ -10,10 +9,6 @@ import envoy.exception.EnvoyException;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Records audio and exports it as a byte array.
|
* Records audio and exports it as a byte array.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>AudioRecorder.java</strong><br>
|
|
||||||
* Created: <strong>02.07.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
@ -27,6 +22,11 @@ public final class AudioRecorder {
|
|||||||
*/
|
*/
|
||||||
public static final AudioFormat DEFAULT_AUDIO_FORMAT = new AudioFormat(16000, 16, 1, true, false);
|
public static final AudioFormat DEFAULT_AUDIO_FORMAT = new AudioFormat(16000, 16, 1, true, false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The format in which audio files will be saved.
|
||||||
|
*/
|
||||||
|
public static final String FILE_FORMAT = "wav";
|
||||||
|
|
||||||
private final AudioFormat format;
|
private final AudioFormat format;
|
||||||
private final DataLine.Info info;
|
private final DataLine.Info info;
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ public final class AudioRecorder {
|
|||||||
line.start();
|
line.start();
|
||||||
|
|
||||||
// Prepare temp file
|
// Prepare temp file
|
||||||
tempFile = Files.createTempFile("recording", "wav");
|
tempFile = Files.createTempFile("recording", FILE_FORMAT);
|
||||||
|
|
||||||
// Start the recording
|
// Start the recording
|
||||||
final var ais = new AudioInputStream(line);
|
final var ais = new AudioInputStream(line);
|
||||||
@ -117,6 +117,6 @@ public final class AudioRecorder {
|
|||||||
line.close();
|
line.close();
|
||||||
try {
|
try {
|
||||||
Files.deleteIfExists(tempFile);
|
Files.deleteIfExists(tempFile);
|
||||||
} catch (IOException e) {}
|
} catch (final IOException e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Contains classes related to recording and playing back audio clips.
|
* Contains classes related to recording and playing back audio clips.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>package-info.java</strong><br>
|
|
||||||
* Created: <strong>05.07.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
|
30
client/src/main/java/envoy/client/data/commands/OnCall.java
Normal file
30
client/src/main/java/envoy/client/data/commands/OnCall.java
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package envoy.client.data.commands;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface defines an action that should be performed when a system
|
||||||
|
* command gets called.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public interface OnCall {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs class specific actions when a {@link SystemCommand} has been called.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
void onCall();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs actions that can only be performed by classes that are not
|
||||||
|
* {@link SystemCommand}s when a SystemCommand has been called.
|
||||||
|
*
|
||||||
|
* @param consumer the action to perform when this {@link SystemCommand} has
|
||||||
|
* been called
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
void onCall(Supplier<Void> consumer);
|
||||||
|
}
|
@ -0,0 +1,128 @@
|
|||||||
|
package envoy.client.data.commands;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is the base class of all {@code SystemCommands} and contains an
|
||||||
|
* action and a number of arguments that should be used as input for this
|
||||||
|
* function.
|
||||||
|
* No {@code SystemCommand} can return anything.
|
||||||
|
* Every {@code SystemCommand} must have as argument type {@code List<String>} so
|
||||||
|
* that the words following the indicator String can be used as input of the
|
||||||
|
* function. This approach has one limitation:<br>
|
||||||
|
* <b>Order matters!</b> Changing the order of arguments will likely result in
|
||||||
|
* unexpected behavior.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public final class SystemCommand implements OnCall {
|
||||||
|
|
||||||
|
protected int relevance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The argument count of the command.
|
||||||
|
*/
|
||||||
|
protected final int numberOfArguments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function takes a {@code List<String>} as argument because automatically
|
||||||
|
* {@code SystemCommand#numberOfArguments} words following the necessary command
|
||||||
|
* will be put into this list.
|
||||||
|
*
|
||||||
|
* @see String#split(String)
|
||||||
|
*/
|
||||||
|
protected final Consumer<List<String>> action;
|
||||||
|
|
||||||
|
protected final String description;
|
||||||
|
|
||||||
|
protected final List<String> defaults;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code SystemCommand}.
|
||||||
|
*
|
||||||
|
* @param action the action performed by the command
|
||||||
|
* @param numberOfArguments the argument count accepted by the action
|
||||||
|
* @param defaults the default values for the corresponding arguments
|
||||||
|
* @param description the description of this {@code SystemCommand}
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommand(Consumer<List<String>> action, int numberOfArguments, List<String> defaults, String description) {
|
||||||
|
this.numberOfArguments = numberOfArguments;
|
||||||
|
this.action = action;
|
||||||
|
this.defaults = defaults == null ? new ArrayList<>() : defaults;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the action that should be performed
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public Consumer<List<String>> getAction() { return action; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the argument count of the command
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public int getNumberOfArguments() { return numberOfArguments; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the description
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the relevance
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public int getRelevance() { return relevance; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param relevance the relevance to set
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public void setRelevance(int relevance) { this.relevance = relevance; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments the relevance of this {@code SystemCommand}.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onCall() { relevance++; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments the relevance of this {@code SystemCommand} and executes the
|
||||||
|
* supplier.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onCall(Supplier<Void> consumer) {
|
||||||
|
onCall();
|
||||||
|
consumer.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the defaults
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public List<String> getDefaults() { return defaults; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() { return Objects.hash(action); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) return true;
|
||||||
|
if (obj == null) return false;
|
||||||
|
if (getClass() != obj.getClass()) return false;
|
||||||
|
final SystemCommand other = (SystemCommand) obj;
|
||||||
|
return Objects.equals(action, other.action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "SystemCommand [relevance=" + relevance + ", numberOfArguments=" + numberOfArguments + ", "
|
||||||
|
+ (action != null ? "action=" + action + ", " : "") + (description != null ? "description=" + description + ", " : "")
|
||||||
|
+ (defaults != null ? "defaults=" + defaults : "") + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,230 @@
|
|||||||
|
package envoy.client.data.commands;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class acts as a builder for {@link SystemCommand}s.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public final class SystemCommandBuilder {
|
||||||
|
|
||||||
|
private int numberOfArguments;
|
||||||
|
private Consumer<List<String>> action;
|
||||||
|
private List<String> defaults;
|
||||||
|
private String description;
|
||||||
|
private int relevance;
|
||||||
|
|
||||||
|
private final SystemCommandMap commandsMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code SystemCommandsBuilder} without underlying
|
||||||
|
* {@link SystemCommandMap}.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommandBuilder() { this(null); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param commandsMap the map to use when calling build (optional)
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommandBuilder(SystemCommandMap commandsMap) { this.commandsMap = commandsMap; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param numberOfArguments the numberOfArguments to set
|
||||||
|
* @return this {@code SystemCommandBuilder}
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommandBuilder setNumberOfArguments(int numberOfArguments) {
|
||||||
|
this.numberOfArguments = numberOfArguments;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param action the action to set
|
||||||
|
* @return this {@code SystemCommandBuilder}
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommandBuilder setAction(Consumer<List<String>> action) {
|
||||||
|
this.action = action;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param description the description to set
|
||||||
|
* @return this {@code SystemCommandBuilder}
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommandBuilder setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param relevance the relevance to set
|
||||||
|
* @return this {@code SystemCommandBuilder}
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommandBuilder setRelevance(int relevance) {
|
||||||
|
this.relevance = relevance;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param defaults the defaults to set
|
||||||
|
* @return this {@code SystemCommandBuilder}
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommandBuilder setDefaults(String... defaults) {
|
||||||
|
this.defaults = List.of(defaults);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets all values stored.
|
||||||
|
*
|
||||||
|
* @return this {@code SystemCommandBuilder}
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommandBuilder reset() {
|
||||||
|
numberOfArguments = 0;
|
||||||
|
action = null;
|
||||||
|
defaults = new ArrayList<>();
|
||||||
|
description = "";
|
||||||
|
relevance = 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@code SystemCommand} based upon the previously entered data.
|
||||||
|
*
|
||||||
|
* @return the built {@code SystemCommand}
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommand build() { return build(true); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
|
||||||
|
* {@code SystemCommand#numberOfArguments} will be set to 0, regardless of the
|
||||||
|
* previous value.<br>
|
||||||
|
* At the end, this {@code SystemCommandBuilder} will be reset.
|
||||||
|
*
|
||||||
|
* @return the built {@code SystemCommand}
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommand buildNoArg() {
|
||||||
|
numberOfArguments = 0;
|
||||||
|
return build(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
|
||||||
|
* {@code SystemCommand#numberOfArguments} will be set to use the rest of the
|
||||||
|
* string as argument, regardless of the previous value.<br>
|
||||||
|
* At the end, this {@code SystemCommandBuilder} will be reset.
|
||||||
|
*
|
||||||
|
* @return the built {@code SystemCommand}
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommand buildRemainingArg() {
|
||||||
|
numberOfArguments = -1;
|
||||||
|
return build(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
|
||||||
|
* Automatically adds the built object to the given map.
|
||||||
|
* At the end, this {@code SystemCommandBuilder} <b>can</b> be reset but must
|
||||||
|
* not be.
|
||||||
|
*
|
||||||
|
* @param reset whether this {@code SystemCommandBuilder} should be reset
|
||||||
|
* afterwards.<br>
|
||||||
|
* This can be useful if another command wants to execute something
|
||||||
|
* similar
|
||||||
|
* @return the built {@code SystemCommand}
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommand build(boolean reset) {
|
||||||
|
final var sc = new SystemCommand(action, numberOfArguments, defaults, description);
|
||||||
|
sc.setRelevance(relevance);
|
||||||
|
if (reset) reset();
|
||||||
|
return sc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@code SystemCommand} based upon the previously entered data.
|
||||||
|
* Automatically adds the built object to the given map.
|
||||||
|
*
|
||||||
|
* @param command the command under which to store the SystemCommand in the
|
||||||
|
* {@link SystemCommandMap}
|
||||||
|
* @return the built {@code SystemCommand}
|
||||||
|
* @throws NullPointerException if no map has been assigned to this builder
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommand build(String command) { return build(command, true); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
|
||||||
|
* Automatically adds the built object to the given map.
|
||||||
|
* {@code SystemCommand#numberOfArguments} will be set to 0, regardless of the
|
||||||
|
* previous value.<br>
|
||||||
|
* At the end, this {@code SystemCommandBuilder} will be reset.
|
||||||
|
*
|
||||||
|
* @param command the command under which to store the SystemCommand in the
|
||||||
|
* {@link SystemCommandMap}
|
||||||
|
* @return the built {@code SystemCommand}
|
||||||
|
* @throws NullPointerException if no map has been assigned to this builder
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommand buildNoArg(String command) {
|
||||||
|
numberOfArguments = 0;
|
||||||
|
return build(command, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
|
||||||
|
* Automatically adds the built object to the given map.
|
||||||
|
* {@code SystemCommand#numberOfArguments} will be set to use the rest of the
|
||||||
|
* string as argument, regardless of the previous value.<br>
|
||||||
|
* At the end, this {@code SystemCommandBuilder} will be reset.
|
||||||
|
*
|
||||||
|
* @param command the command under which to store the SystemCommand in the
|
||||||
|
* {@link SystemCommandMap}
|
||||||
|
* @return the built {@code SystemCommand}
|
||||||
|
* @throws NullPointerException if no map has been assigned to this builder
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommand buildRemainingArg(String command) {
|
||||||
|
numberOfArguments = -1;
|
||||||
|
return build(command, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
|
||||||
|
* Automatically adds the built object to the given map.
|
||||||
|
* At the end, this {@code SystemCommandBuilder} <b>can</b> be reset but must
|
||||||
|
* not be.
|
||||||
|
*
|
||||||
|
* @param command the command under which to store the SystemCommand in the
|
||||||
|
* {@link SystemCommandMap}
|
||||||
|
* @param reset whether this {@code SystemCommandBuilder} should be reset
|
||||||
|
* afterwards.<br>
|
||||||
|
* This can be useful if another command wants to execute
|
||||||
|
* something
|
||||||
|
* similar
|
||||||
|
* @return the built {@code SystemCommand}
|
||||||
|
* @throws NullPointerException if no map has been assigned to this builder
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public SystemCommand build(String command, boolean reset) {
|
||||||
|
final var sc = new SystemCommand(action, numberOfArguments, defaults, description);
|
||||||
|
sc.setRelevance(relevance);
|
||||||
|
if (commandsMap != null) commandsMap.add(command, sc);
|
||||||
|
else throw new NullPointerException("No map in SystemCommandsBuilder present");
|
||||||
|
if (reset) reset();
|
||||||
|
return sc;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,260 @@
|
|||||||
|
package envoy.client.data.commands;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.logging.*;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import envoy.util.EnvoyLog;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class stores all {@link SystemCommand}s used.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public final class SystemCommandMap {
|
||||||
|
|
||||||
|
private final Map<String, SystemCommand> systemCommands = new HashMap<>();
|
||||||
|
|
||||||
|
private final Pattern commandPattern = Pattern.compile("^[a-zA-Z0-9_:!\\(\\)\\?\\.\\,\\;\\-]+$");
|
||||||
|
|
||||||
|
private static final Logger logger = EnvoyLog.getLogger(SystemCommandMap.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new command to the map if the command name is valid.
|
||||||
|
*
|
||||||
|
* @param command the input string to execute the
|
||||||
|
* given action
|
||||||
|
* @param systemCommand the command to add - can be built using
|
||||||
|
* {@link SystemCommandBuilder}
|
||||||
|
* @see SystemCommandMap#isValidKey(String)
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public void add(String command, SystemCommand systemCommand) {
|
||||||
|
if (isValidKey(command)) systemCommands.put(command.toLowerCase(), systemCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method checks if the input String is a key in the map and returns the
|
||||||
|
* wrapped System command if present.
|
||||||
|
* It will return an empty optional if the value after the slash is not a key in
|
||||||
|
* the map, which is a valid case (i.e. input="3/4" and "4" is not a key in the
|
||||||
|
* map).
|
||||||
|
* <p>
|
||||||
|
* Usage example:<br>
|
||||||
|
* {@code SystemCommandMap systemCommands = new SystemCommandMap();}<br>
|
||||||
|
* {@code Button button = new Button();}
|
||||||
|
* {@code systemCommands.add("example", text -> button.setText(text.get(0), 1);}<br>
|
||||||
|
* {@code ....}<br>
|
||||||
|
* user input: {@code "/example xyz ..."}<br>
|
||||||
|
* {@code systemCommands.get("example xyz ...")} or
|
||||||
|
* {@code systemCommands.get("/example xyz ...")}
|
||||||
|
* result: {@code Optional<SystemCommand>}
|
||||||
|
*
|
||||||
|
* @param input the input string given by the user
|
||||||
|
* @return the wrapped system command, if present
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public Optional<SystemCommand> get(String input) { return Optional.ofNullable(systemCommands.get(getCommand(input.toLowerCase()))); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method ensures that the "/" of a {@link SystemCommand} is stripped.<br>
|
||||||
|
* It returns the command as (most likely) entered as key in the map for the
|
||||||
|
* first word of the text.<br>
|
||||||
|
* It should only be called on strings that contain a "/" at position 0/-1.
|
||||||
|
*
|
||||||
|
* @param raw the input
|
||||||
|
* @return the command as entered in the map
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
* @apiNote this method will (most likely) not return anything useful if
|
||||||
|
* whatever is entered after the slash is not a system command. Only
|
||||||
|
* exception: for recommendation purposes.
|
||||||
|
*/
|
||||||
|
public String getCommand(String raw) {
|
||||||
|
final var trimmed = raw.stripLeading();
|
||||||
|
final var index = trimmed.indexOf(' ');
|
||||||
|
return trimmed.substring(trimmed.charAt(0) == '/' ? 1 : 0, index < 1 ? trimmed.length() : index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Examines whether a key can be put in the map and logs it with
|
||||||
|
* {@code Level.WARNING} if that key violates API constrictions.<br>
|
||||||
|
* (allowed chars are <b>a-zA-Z0-9_:!()?.,;-</b>)
|
||||||
|
* <p>
|
||||||
|
* The approach to not throw an exception was taken so that an ugly try-catch
|
||||||
|
* block for every addition to the system commands map could be avoided, an
|
||||||
|
* error that should only occur during implementation and not in production.
|
||||||
|
*
|
||||||
|
* @param command the key to examine
|
||||||
|
* @return whether this key can be used in the map
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public boolean isValidKey(String command) {
|
||||||
|
final boolean valid = commandPattern.matcher(command).matches();
|
||||||
|
if (!valid) logger.log(Level.WARNING,
|
||||||
|
"The command \"" + command
|
||||||
|
+ "\" is not valid. As it will cause problems in execution, it will not be entered into the map. Only the characters "
|
||||||
|
+ commandPattern + "are allowed");
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a 'raw' string (the whole input) and checks if "/" is the first visible
|
||||||
|
* character and then checks if a command is present after that "/". If that is
|
||||||
|
* the case, it will be executed.
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* @param raw the raw input string
|
||||||
|
* @return whether a command could be found
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public boolean executeIfAnyPresent(String raw) {
|
||||||
|
// possibly a command was detected and could be executed
|
||||||
|
final var raw2 = raw.stripLeading();
|
||||||
|
final var commandFound = raw2.startsWith("/") ? executeIfPresent(raw2) : false;
|
||||||
|
// the command was executed successfully - no further checking needed
|
||||||
|
if (commandFound) logger.log(Level.FINE, "executed system command " + getCommand(raw2));
|
||||||
|
return commandFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method checks if the input String is a key in the map and executes the
|
||||||
|
* wrapped System command if present.
|
||||||
|
* Its intended usage is after a "/" has been detected in the input String.
|
||||||
|
* It will do nothing if the value after the slash is not a key in
|
||||||
|
* the map, which is a valid case (i.e. input="3/4" and "4" is not a key in the
|
||||||
|
* map).
|
||||||
|
* <p>
|
||||||
|
* Usage example:<br>
|
||||||
|
* {@code SystemCommandMap systemCommands = new SystemCommandMap();}<br>
|
||||||
|
* {@code Button button = new Button();}<br>
|
||||||
|
* {@code systemCommands.add("example", (words)-> button.setText(words.get(0), 1);}<br>
|
||||||
|
* {@code ....}<br>
|
||||||
|
* user input: {@code "/example xyz ..."}<br>
|
||||||
|
* {@code systemCommands.executeIfPresent("example xyz ...")}
|
||||||
|
* result: {@code button.getText()=="xyz"}
|
||||||
|
*
|
||||||
|
* @param input the input string given by the user
|
||||||
|
* @return whether a command could be found
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public boolean executeIfPresent(String input) {
|
||||||
|
final var command = getCommand(input);
|
||||||
|
final var value = get(command);
|
||||||
|
value.ifPresent(systemCommand -> {
|
||||||
|
// Splitting the String so that the leading command including the first " " is
|
||||||
|
// removed and only as many following words as allowed by the system command
|
||||||
|
// persist
|
||||||
|
final var arguments = extractArguments(input, systemCommand);
|
||||||
|
// Executing the function
|
||||||
|
try {
|
||||||
|
systemCommand.getAction().accept(arguments);
|
||||||
|
systemCommand.onCall();
|
||||||
|
} catch (final Exception e) {
|
||||||
|
logger.log(Level.WARNING, "The system command " + command + " threw an exception: ", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return value.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supplies missing values with default values.
|
||||||
|
*
|
||||||
|
* @param input the input String
|
||||||
|
* @param systemCommand the command that is expected
|
||||||
|
* @return the list of arguments that can be used to parse the systemCommand
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
private List<String> extractArguments(String input, SystemCommand systemCommand) {
|
||||||
|
// no more arguments follow after the command (e.g. text = "/DABR")
|
||||||
|
final var indexOfSpace = input.indexOf(" ");
|
||||||
|
if (indexOfSpace < 0) return supplementDefaults(new String[] {}, systemCommand);
|
||||||
|
// the arguments behind a system command
|
||||||
|
final var remainingString = input.substring(indexOfSpace + 1);
|
||||||
|
final var numberOfArguments = systemCommand.getNumberOfArguments();
|
||||||
|
// splitting those arguments and supplying default values
|
||||||
|
final var textArguments = remainingString.split(" ", -1);
|
||||||
|
final var originalArguments = numberOfArguments >= 0 ? Arrays.copyOfRange(textArguments, 0, numberOfArguments) : textArguments;
|
||||||
|
final var arguments = supplementDefaults(originalArguments, systemCommand);
|
||||||
|
return arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the recommendations based on the current input entered.<br>
|
||||||
|
* The first word is used for the recommendations and
|
||||||
|
* it does not matter if the "/" is at its beginning or not.<br>
|
||||||
|
* If none are present, nothing will be done.<br>
|
||||||
|
* Otherwise the given function will be executed on the recommendations.<br>
|
||||||
|
*
|
||||||
|
* @param input the input string
|
||||||
|
* @param action the action that should be taken for the recommendations, if any
|
||||||
|
* are present
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public void requestRecommendations(String input, Consumer<Set<String>> action) {
|
||||||
|
final var partialCommand = getCommand(input);
|
||||||
|
// Get the expected commands
|
||||||
|
final var recommendations = recommendCommands(partialCommand);
|
||||||
|
if (recommendations.isEmpty()) return;
|
||||||
|
// Execute the given action
|
||||||
|
else action.accept(recommendations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recommends commands based upon the currently entered input.<br>
|
||||||
|
* In the current implementation, all we check is whether a key contains this
|
||||||
|
* input. This might be updated later on.
|
||||||
|
*
|
||||||
|
* @param partialCommand the partially entered command
|
||||||
|
* @return a set of all commands that match this input
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
private Set<String> recommendCommands(String partialCommand) {
|
||||||
|
// current implementation only looks if input is contained within a command,
|
||||||
|
// might be updated
|
||||||
|
return systemCommands.keySet()
|
||||||
|
.stream()
|
||||||
|
.filter(command -> command.contains(partialCommand))
|
||||||
|
.sorted((command1, command2) -> Integer.compare(systemCommands.get(command1).getRelevance(), systemCommands.get(command2).getRelevance()))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Supplies the default values for arguments if none are present in the text for
|
||||||
|
* any argument. <br>
|
||||||
|
* Will only work for {@code SystemCommand}s whose argument counter is bigger
|
||||||
|
* than 1.
|
||||||
|
*
|
||||||
|
* @param textArguments the arguments that were parsed from the text
|
||||||
|
* @param toEvaluate the system command whose default values should be used
|
||||||
|
* @return the final argument list
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
* @apiNote this method will insert an empty String if the size of the list
|
||||||
|
* given to the {@code SystemCommand} is smaller than its argument
|
||||||
|
* counter and no more text arguments could be found.
|
||||||
|
*/
|
||||||
|
private List<String> supplementDefaults(String[] textArguments, SystemCommand toEvaluate) {
|
||||||
|
final var defaults = toEvaluate.getDefaults();
|
||||||
|
final var numberOfArguments = toEvaluate.getNumberOfArguments();
|
||||||
|
final List<String> result = new ArrayList<>();
|
||||||
|
|
||||||
|
if (toEvaluate.getNumberOfArguments() > 0) for (int index = 0; index < numberOfArguments; index++) {
|
||||||
|
String textArg = null;
|
||||||
|
if (index < textArguments.length) textArg = textArguments[index];
|
||||||
|
// Set the argument at position index to the current argument of the text, if it
|
||||||
|
// is present. Otherwise the default for that argument will be taken if present.
|
||||||
|
// In the worst case, an empty String will be used.
|
||||||
|
result.add(!(textArg == null) && !textArg.isBlank() ? textArg : index < defaults.size() ? defaults.get(index) : "");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return all {@link SystemCommand}s used with the underlying command as key
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public Map<String, SystemCommand> getSystemCommands() { return systemCommands; }
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* This package contains all classes that can be used as system commands.<br>
|
||||||
|
* Every system command can be called using a specific syntax:"/<command>"
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
package envoy.client.data.commands;
|
15
client/src/main/java/envoy/client/event/BackEvent.java
Normal file
15
client/src/main/java/envoy/client/event/BackEvent.java
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package envoy.client.event;
|
||||||
|
|
||||||
|
import envoy.event.Event.Valueless;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event serves the purpose of triggering the tab change to tab 0 in
|
||||||
|
* {@link envoy.client.ui.controller.ChatScene}.
|
||||||
|
*
|
||||||
|
* @author Maximilian Käfer
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public class BackEvent extends Valueless {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 0L;
|
||||||
|
}
|
16
client/src/main/java/envoy/client/event/EnvoyCloseEvent.java
Normal file
16
client/src/main/java/envoy/client/event/EnvoyCloseEvent.java
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package envoy.client.event;
|
||||||
|
|
||||||
|
import envoy.event.Event.Valueless;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event notifies various Envoy components of the application being about
|
||||||
|
* to shut down. This allows the graceful closing of connections, persisting
|
||||||
|
* local data etc.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public class EnvoyCloseEvent extends Valueless {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
}
|
14
client/src/main/java/envoy/client/event/Logout.java
Normal file
14
client/src/main/java/envoy/client/event/Logout.java
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package envoy.client.event;
|
||||||
|
|
||||||
|
import envoy.event.Event.Valueless;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that a logout has been requested.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public final class Logout extends Valueless {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
}
|
@ -1,22 +0,0 @@
|
|||||||
package envoy.client.event;
|
|
||||||
|
|
||||||
import envoy.data.Message;
|
|
||||||
import envoy.event.Event;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>MessageCreationEvent.java</strong><br>
|
|
||||||
* Created: <strong>4 Dec 2019</strong><br>
|
|
||||||
*
|
|
||||||
* @author Kai S. K. Engelbart
|
|
||||||
* @since Envoy Client v0.2-alpha
|
|
||||||
*/
|
|
||||||
public class MessageCreationEvent extends Event<Message> {
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 0L;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param message the {@link Message} that has been created
|
|
||||||
*/
|
|
||||||
public MessageCreationEvent(Message message) { super(message); }
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
package envoy.client.event;
|
|
||||||
|
|
||||||
import envoy.data.Message;
|
|
||||||
import envoy.event.Event;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>MessageModificationEvent.java</strong><br>
|
|
||||||
* Created: <strong>4 Dec 2019</strong><br>
|
|
||||||
*
|
|
||||||
* @author Kai S. K. Engelbart
|
|
||||||
* @since Envoy Client v0.2-alpha
|
|
||||||
*/
|
|
||||||
public class MessageModificationEvent extends Event<Message> {
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 0L;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param message the {@link Message} that has been modified
|
|
||||||
*/
|
|
||||||
public MessageModificationEvent(Message message) { super(message); }
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
package envoy.client.event;
|
|
||||||
|
|
||||||
import envoy.event.Event;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>SendEvent.java</strong><br>
|
|
||||||
* Created: <strong>11.02.2020</strong><br>
|
|
||||||
*
|
|
||||||
* @author: Maximilian Käfer
|
|
||||||
* @since Envoy Client v0.3-alpha
|
|
||||||
*/
|
|
||||||
public class SendEvent extends Event<Event<?>> {
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 0L;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param value the event to send to the server
|
|
||||||
*/
|
|
||||||
public SendEvent(Event<?> value) { super(value); }
|
|
||||||
|
|
||||||
}
|
|
@ -3,23 +3,12 @@ package envoy.client.event;
|
|||||||
import envoy.event.Event;
|
import envoy.event.Event;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project: <strong>envoy-client</strong><br>
|
* Notifies UI components of a theme change.
|
||||||
* File: <strong>ThemeChangeEvent.java</strong><br>
|
|
||||||
* Created: <strong>15 Dec 2019</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.2-alpha
|
* @since Envoy Client v0.2-alpha
|
||||||
*/
|
*/
|
||||||
public class ThemeChangeEvent extends Event<String> {
|
public final class ThemeChangeEvent extends Event.Valueless {
|
||||||
|
|
||||||
private static final long serialVersionUID = 0L;
|
private static final long serialVersionUID = 0L;
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes a {@link ThemeChangeEvent} conveying information about the change
|
|
||||||
* of the theme currently in use.
|
|
||||||
*
|
|
||||||
* @param theme the name of the new theme
|
|
||||||
* @since Envoy Client v0.2-alpha
|
|
||||||
*/
|
|
||||||
public ThemeChangeEvent(String theme) { super(theme); }
|
|
||||||
}
|
}
|
||||||
|
36
client/src/main/java/envoy/client/helper/AlertHelper.java
Normal file
36
client/src/main/java/envoy/client/helper/AlertHelper.java
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package envoy.client.helper;
|
||||||
|
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
|
||||||
|
import envoy.client.data.Settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides methods that are commonly used for alerts.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public final class AlertHelper {
|
||||||
|
|
||||||
|
private AlertHelper() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asks for a confirmation dialog if {@link Settings#isAskForConfirmation()}
|
||||||
|
* returns {@code true}.
|
||||||
|
* Immediately executes the action if no dialog was requested or the dialog was
|
||||||
|
* exited with a confirmation.
|
||||||
|
* Does nothing if the dialog was closed without clicking on OK.
|
||||||
|
*
|
||||||
|
* @param alert the (customized) alert to show. <strong>Should not be shown
|
||||||
|
* already</strong>
|
||||||
|
* @param action the action to perform in case of success
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public static void confirmAction(Alert alert, Runnable action) {
|
||||||
|
alert.setHeight(225);
|
||||||
|
alert.setWidth(400);
|
||||||
|
alert.setHeaderText("");
|
||||||
|
if (Settings.getInstance().isAskForConfirmation()) alert.showAndWait().filter(ButtonType.OK::equals).ifPresent(bu -> action.run());
|
||||||
|
else action.run();
|
||||||
|
}
|
||||||
|
}
|
57
client/src/main/java/envoy/client/helper/ShutdownHelper.java
Normal file
57
client/src/main/java/envoy/client/helper/ShutdownHelper.java
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package envoy.client.helper;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.Alert.AlertType;
|
||||||
|
|
||||||
|
import envoy.client.data.*;
|
||||||
|
import envoy.client.event.*;
|
||||||
|
import envoy.client.ui.SceneContext.SceneInfo;
|
||||||
|
import envoy.util.EnvoyLog;
|
||||||
|
|
||||||
|
import dev.kske.eventbus.EventBus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplifies shutdown actions.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public final class ShutdownHelper {
|
||||||
|
|
||||||
|
private ShutdownHelper() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exits Envoy or minimizes it, depending on the current state of
|
||||||
|
* {@link Settings#isHideOnClose()}.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public static void exit() {
|
||||||
|
if (Settings.getInstance().isHideOnClose()) Context.getInstance().getStage().setIconified(true);
|
||||||
|
else {
|
||||||
|
EventBus.getInstance().dispatch(new EnvoyCloseEvent());
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the current user out and reopens
|
||||||
|
* {@link envoy.client.ui.controller.LoginScene}.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public static void logout() {
|
||||||
|
final var alert = new Alert(AlertType.CONFIRMATION);
|
||||||
|
alert.setTitle("Logout?");
|
||||||
|
alert.setContentText("Are you sure you want to log out?");
|
||||||
|
|
||||||
|
AlertHelper.confirmAction(alert, () -> {
|
||||||
|
EnvoyLog.getLogger(ShutdownHelper.class).log(Level.INFO, "A logout was requested");
|
||||||
|
EventBus.getInstance().dispatch(new EnvoyCloseEvent());
|
||||||
|
EventBus.getInstance().dispatch(new Logout());
|
||||||
|
Context.getInstance().getSceneContext().load(SceneInfo.LOGIN_SCENE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Provides helper methods that reduce boilerplate code.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @author Kai S. K. Engelbert
|
||||||
|
* @author Maximilian Käfer
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
package envoy.client.helper;
|
@ -1,35 +1,29 @@
|
|||||||
package envoy.client.net;
|
package envoy.client.net;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.*;
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.*;
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import envoy.client.data.*;
|
import envoy.client.data.*;
|
||||||
import envoy.client.event.SendEvent;
|
import envoy.client.event.EnvoyCloseEvent;
|
||||||
import envoy.data.*;
|
import envoy.data.*;
|
||||||
import envoy.event.*;
|
import envoy.event.*;
|
||||||
import envoy.event.contact.ContactOperation;
|
import envoy.util.*;
|
||||||
import envoy.event.contact.ContactSearchResult;
|
|
||||||
import envoy.util.EnvoyLog;
|
import dev.kske.eventbus.*;
|
||||||
import envoy.util.SerializationUtils;
|
import dev.kske.eventbus.Event;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Establishes a connection to the server, performs a handshake and delivers
|
* Establishes a connection to the server, performs a handshake and delivers
|
||||||
* certain objects to the server.
|
* certain objects to the server.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>Client.java</strong><br>
|
|
||||||
* Created: <strong>28 Sep 2019</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @author Maximilian Käfer
|
* @author Maximilian Käfer
|
||||||
* @author Leon Hofmeister
|
* @author Leon Hofmeister
|
||||||
* @since Envoy Client v0.1-alpha
|
* @since Envoy Client v0.1-alpha
|
||||||
*/
|
*/
|
||||||
public class Client implements Closeable {
|
public final class Client implements EventListener, Closeable {
|
||||||
|
|
||||||
// Connection handling
|
// Connection handling
|
||||||
private Socket socket;
|
private Socket socket;
|
||||||
@ -45,6 +39,13 @@ public class Client implements Closeable {
|
|||||||
private static final Logger logger = EnvoyLog.getLogger(Client.class);
|
private static final Logger logger = EnvoyLog.getLogger(Client.class);
|
||||||
private static final EventBus eventBus = EventBus.getInstance();
|
private static final EventBus eventBus = EventBus.getInstance();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a client and registers it as an event listener.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public Client() { eventBus.registerListener(this); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enters the online mode by acquiring a user ID from the server. As a
|
* Enters the online mode by acquiring a user ID from the server. As a
|
||||||
* connection has to be established and a handshake has to be made, this method
|
* connection has to be established and a handshake has to be made, this method
|
||||||
@ -69,10 +70,10 @@ public class Client implements Closeable {
|
|||||||
// Create object receiver
|
// Create object receiver
|
||||||
receiver = new Receiver(socket.getInputStream());
|
receiver = new Receiver(socket.getInputStream());
|
||||||
|
|
||||||
// Register user creation processor, contact list processor and message cache
|
// Register user creation processor, contact list processor, message cache and
|
||||||
|
// authentication token
|
||||||
receiver.registerProcessor(User.class, sender -> this.sender = sender);
|
receiver.registerProcessor(User.class, sender -> this.sender = sender);
|
||||||
receiver.registerProcessors(cacheMap.getMap());
|
receiver.registerProcessors(cacheMap.getMap());
|
||||||
receiver.registerProcessor(HandshakeRejection.class, evt -> { rejected = true; eventBus.dispatch(evt); });
|
|
||||||
|
|
||||||
rejected = false;
|
rejected = false;
|
||||||
|
|
||||||
@ -99,7 +100,6 @@ public class Client implements Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
online = true;
|
online = true;
|
||||||
|
|
||||||
logger.log(Level.INFO, "Handshake completed.");
|
logger.log(Level.INFO, "Handshake completed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,99 +120,86 @@ public class Client implements Closeable {
|
|||||||
// Remove all processors as they are only used during the handshake
|
// Remove all processors as they are only used during the handshake
|
||||||
receiver.removeAllProcessors();
|
receiver.removeAllProcessors();
|
||||||
|
|
||||||
// Process incoming messages
|
|
||||||
final var receivedMessageProcessor = new ReceivedMessageProcessor();
|
|
||||||
final var receivedGroupMessageProcessor = new ReceivedGroupMessageProcessor();
|
|
||||||
final var messageStatusChangeProcessor = new MessageStatusChangeProcessor();
|
|
||||||
final var groupMessageStatusChangeProcessor = new GroupMessageStatusChangeProcessor();
|
|
||||||
|
|
||||||
receiver.registerProcessor(GroupMessage.class, receivedGroupMessageProcessor);
|
|
||||||
receiver.registerProcessor(Message.class, receivedMessageProcessor);
|
|
||||||
receiver.registerProcessor(MessageStatusChange.class, messageStatusChangeProcessor);
|
|
||||||
receiver.registerProcessor(GroupMessageStatusChange.class, groupMessageStatusChangeProcessor);
|
|
||||||
|
|
||||||
// Relay cached messages and message status changes
|
// Relay cached messages and message status changes
|
||||||
cacheMap.get(Message.class).setProcessor(receivedMessageProcessor);
|
cacheMap.get(Message.class).setProcessor(eventBus::dispatch);
|
||||||
cacheMap.get(GroupMessage.class).setProcessor(receivedGroupMessageProcessor);
|
cacheMap.get(GroupMessage.class).setProcessor(eventBus::dispatch);
|
||||||
cacheMap.get(MessageStatusChange.class).setProcessor(messageStatusChangeProcessor);
|
cacheMap.get(MessageStatusChange.class).setProcessor(eventBus::dispatch);
|
||||||
cacheMap.get(GroupMessageStatusChange.class).setProcessor(groupMessageStatusChangeProcessor);
|
cacheMap.get(GroupMessageStatusChange.class).setProcessor(eventBus::dispatch);
|
||||||
|
|
||||||
// Process user status changes
|
|
||||||
receiver.registerProcessor(UserStatusChange.class, eventBus::dispatch);
|
|
||||||
|
|
||||||
// Process message ID generation
|
|
||||||
receiver.registerProcessor(IDGenerator.class, localDB::setIDGenerator);
|
|
||||||
|
|
||||||
// Process name changes
|
|
||||||
receiver.registerProcessor(NameChange.class, evt -> { localDB.replaceContactName(evt); eventBus.dispatch(evt); });
|
|
||||||
|
|
||||||
// Process contact searches
|
|
||||||
receiver.registerProcessor(ContactSearchResult.class, eventBus::dispatch);
|
|
||||||
|
|
||||||
// Process contact operations
|
|
||||||
receiver.registerProcessor(ContactOperation.class, eventBus::dispatch);
|
|
||||||
|
|
||||||
// Process group size changes
|
|
||||||
receiver.registerProcessor(GroupResize.class, evt -> { localDB.updateGroup(evt); eventBus.dispatch(evt); });
|
|
||||||
|
|
||||||
// Send event
|
|
||||||
eventBus.register(SendEvent.class, evt -> {
|
|
||||||
try {
|
|
||||||
sendEvent(evt.get());
|
|
||||||
} catch (final IOException e) {
|
|
||||||
logger.log(Level.WARNING, "An error occurred when trying to send " + evt, e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Request a generator if none is present or the existing one is consumed
|
// Request a generator if none is present or the existing one is consumed
|
||||||
if (!localDB.hasIDGenerator() || !localDB.getIDGenerator().hasNext()) requestIdGenerator();
|
if (!localDB.hasIDGenerator() || !localDB.getIDGenerator().hasNext()) requestIDGenerator();
|
||||||
|
|
||||||
// Relay caches
|
// Relay caches
|
||||||
cacheMap.getMap().values().forEach(Cache::relay);
|
cacheMap.getMap().values().forEach(Cache::relay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an object to the server.
|
||||||
|
*
|
||||||
|
* @param obj the object to send
|
||||||
|
* @throws IllegalStateException if the client is not online
|
||||||
|
* @throws RuntimeException if the object serialization failed
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public void send(Serializable obj) throws IllegalStateException, RuntimeException {
|
||||||
|
checkOnline();
|
||||||
|
logger.log(Level.FINE, "Sending " + obj);
|
||||||
|
try {
|
||||||
|
SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a message to the server. The message's status will be incremented once
|
* Sends a message to the server. The message's status will be incremented once
|
||||||
* it was delivered successfully.
|
* it was delivered successfully.
|
||||||
*
|
*
|
||||||
* @param message the message to send
|
* @param message the message to send
|
||||||
* @throws IOException if the message does not reach the server
|
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public void sendMessage(Message message) throws IOException {
|
public void sendMessage(Message message) {
|
||||||
writeObject(message);
|
send(message);
|
||||||
message.nextStatus();
|
message.nextStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an event to the server.
|
|
||||||
*
|
|
||||||
* @param evt the event to send
|
|
||||||
* @throws IOException if the event did not reach the server
|
|
||||||
*/
|
|
||||||
public void sendEvent(Event<?> evt) throws IOException { writeObject(evt); }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests a new {@link IDGenerator} from the server.
|
* Requests a new {@link IDGenerator} from the server.
|
||||||
*
|
*
|
||||||
* @throws IOException if the request does not reach the server
|
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public void requestIdGenerator() throws IOException {
|
public void requestIDGenerator() {
|
||||||
logger.log(Level.INFO, "Requesting new id generator...");
|
logger.log(Level.INFO, "Requesting new id generator...");
|
||||||
writeObject(new IDGeneratorRequest());
|
send(new IDGeneratorRequest());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Event(eventType = HandshakeRejection.class, priority = 1000)
|
||||||
|
private void onHandshakeRejection() { rejected = true; }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException { if (online) socket.close(); }
|
@Event(eventType = EnvoyCloseEvent.class, priority = 800)
|
||||||
|
public void close() {
|
||||||
|
if (online) {
|
||||||
|
logger.log(Level.INFO, "Closing connection...");
|
||||||
|
try {
|
||||||
|
|
||||||
private void writeObject(Object obj) throws IOException {
|
// The sender must be reset as otherwise the handshake is immediately closed
|
||||||
checkOnline();
|
sender = null;
|
||||||
logger.log(Level.FINE, "Sending " + obj);
|
online = false;
|
||||||
SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream());
|
socket.close();
|
||||||
|
} catch (final IOException e) {
|
||||||
|
logger.log(Level.WARNING, "Failed to close socket: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkOnline() { if (!online) throw new IllegalStateException("Client is not online"); }
|
/**
|
||||||
|
* Ensured that the client is online.
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException if the client is not online
|
||||||
|
* @since Envoy Client v0.3-alpha
|
||||||
|
*/
|
||||||
|
private void checkOnline() throws IllegalStateException { if (!online) throw new IllegalStateException("Client is not online"); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the {@link User} as which this client is logged in
|
* @return the {@link User} as which this client is logged in
|
||||||
@ -230,6 +217,7 @@ public class Client implements Closeable {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the {@link Receiver} used by this {@link Client}
|
* @return the {@link Receiver} used by this {@link Client}
|
||||||
|
* @since v0.2-alpha
|
||||||
*/
|
*/
|
||||||
public Receiver getReceiver() { return receiver; }
|
public Receiver getReceiver() { return receiver; }
|
||||||
|
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
package envoy.client.net;
|
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import envoy.data.Message.MessageStatus;
|
|
||||||
import envoy.event.EventBus;
|
|
||||||
import envoy.event.GroupMessageStatusChange;
|
|
||||||
import envoy.util.EnvoyLog;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>GroupMessageStatusChangePocessor.java</strong><br>
|
|
||||||
* Created: <strong>03.07.2020</strong><br>
|
|
||||||
*
|
|
||||||
* @author Maximilian Käfer
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public class GroupMessageStatusChangeProcessor implements Consumer<GroupMessageStatusChange> {
|
|
||||||
|
|
||||||
private static final Logger logger = EnvoyLog.getLogger(GroupMessageStatusChangeProcessor.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void accept(GroupMessageStatusChange evt) {
|
|
||||||
if (evt.get().ordinal() < MessageStatus.RECEIVED.ordinal()) logger.warning("Received invalid group message status change " + evt);
|
|
||||||
else EventBus.getInstance().dispatch(evt);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
package envoy.client.net;
|
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import envoy.data.Message.MessageStatus;
|
|
||||||
import envoy.event.EventBus;
|
|
||||||
import envoy.event.MessageStatusChange;
|
|
||||||
import envoy.util.EnvoyLog;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>MessageStatusChangeProcessor.java</strong><br>
|
|
||||||
* Created: <strong>4 Feb 2020</strong><br>
|
|
||||||
*
|
|
||||||
* @author Kai S. K. Engelbart
|
|
||||||
* @since Envoy Client v0.3-alpha
|
|
||||||
*/
|
|
||||||
public class MessageStatusChangeProcessor implements Consumer<MessageStatusChange> {
|
|
||||||
|
|
||||||
private static final Logger logger = EnvoyLog.getLogger(MessageStatusChangeProcessor.class);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches a {@link MessageStatusChange} if the status is
|
|
||||||
* {@code RECEIVED} or {@code READ}.
|
|
||||||
*
|
|
||||||
* @param evt the status change event
|
|
||||||
* @since Envoy Client v0.3-alpha
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void accept(MessageStatusChange evt) {
|
|
||||||
if (evt.get().ordinal() < MessageStatus.RECEIVED.ordinal()) logger.warning("Received invalid message status change " + evt);
|
|
||||||
else EventBus.getInstance().dispatch(evt);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
package envoy.client.net;
|
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import envoy.client.event.MessageCreationEvent;
|
|
||||||
import envoy.data.GroupMessage;
|
|
||||||
import envoy.data.Message.MessageStatus;
|
|
||||||
import envoy.event.EventBus;
|
|
||||||
import envoy.util.EnvoyLog;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>ReceivedGroupMessageProcessor.java</strong><br>
|
|
||||||
* Created: <strong>13.06.2020</strong><br>
|
|
||||||
*
|
|
||||||
* @author Maximilian Käfer
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public class ReceivedGroupMessageProcessor implements Consumer<GroupMessage> {
|
|
||||||
|
|
||||||
private static final Logger logger = EnvoyLog.getLogger(ReceivedGroupMessageProcessor.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void accept(GroupMessage groupMessage) {
|
|
||||||
if (groupMessage.getStatus() == MessageStatus.WAITING || groupMessage.getStatus() == MessageStatus.READ)
|
|
||||||
logger.warning("The groupMessage has the unexpected status " + groupMessage.getStatus());
|
|
||||||
|
|
||||||
// Dispatch event
|
|
||||||
EventBus.getInstance().dispatch(new MessageCreationEvent(groupMessage));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
package envoy.client.net;
|
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import envoy.client.event.MessageCreationEvent;
|
|
||||||
import envoy.data.Message;
|
|
||||||
import envoy.data.Message.MessageStatus;
|
|
||||||
import envoy.event.EventBus;
|
|
||||||
import envoy.util.EnvoyLog;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>ReceivedMessageProcessor.java</strong><br>
|
|
||||||
* Created: <strong>31.12.2019</strong><br>
|
|
||||||
*
|
|
||||||
* @author Kai S. K. Engelbart
|
|
||||||
* @since Envoy Client v0.3-alpha
|
|
||||||
*/
|
|
||||||
public class ReceivedMessageProcessor implements Consumer<Message> {
|
|
||||||
|
|
||||||
private static final Logger logger = EnvoyLog.getLogger(ReceivedMessageProcessor.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void accept(Message message) {
|
|
||||||
if (message.getStatus() != MessageStatus.SENT) logger.log(Level.WARNING, "The message has the unexpected status " + message.getStatus());
|
|
||||||
else {
|
|
||||||
// Update status to RECEIVED
|
|
||||||
message.nextStatus();
|
|
||||||
|
|
||||||
// Dispatch event
|
|
||||||
EventBus.getInstance().dispatch(new MessageCreationEvent(message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +1,31 @@
|
|||||||
package envoy.client.net;
|
package envoy.client.net;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.*;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.ObjectInputStream;
|
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.*;
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import envoy.util.EnvoyLog;
|
import envoy.util.*;
|
||||||
import envoy.util.SerializationUtils;
|
|
||||||
|
import dev.kske.eventbus.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receives objects from the server and passes them to processor objects based
|
* Receives objects from the server and passes them to processor objects based
|
||||||
* on their class.
|
* on their class.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>Receiver.java</strong><br>
|
|
||||||
* Created: <strong>30.12.2019</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public class Receiver extends Thread {
|
public final class Receiver extends Thread {
|
||||||
|
|
||||||
|
private boolean isAlive = true;
|
||||||
|
|
||||||
private final InputStream in;
|
private final InputStream in;
|
||||||
private final Map<Class<?>, Consumer<?>> processors = new HashMap<>();
|
private final Map<Class<?>, Consumer<?>> processors = new HashMap<>();
|
||||||
|
|
||||||
private static final Logger logger = EnvoyLog.getLogger(Receiver.class);
|
private static final EventBus eventBus = EventBus.getInstance();
|
||||||
|
private static final Logger logger = EnvoyLog.getLogger(Receiver.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of {@link Receiver}.
|
* Creates an instance of {@link Receiver}.
|
||||||
@ -40,6 +36,7 @@ public class Receiver extends Thread {
|
|||||||
public Receiver(InputStream in) {
|
public Receiver(InputStream in) {
|
||||||
super("Receiver");
|
super("Receiver");
|
||||||
this.in = in;
|
this.in = in;
|
||||||
|
setDaemon(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,7 +48,7 @@ public class Receiver extends Thread {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
while (true) {
|
while (isAlive)
|
||||||
try {
|
try {
|
||||||
// Read object length
|
// Read object length
|
||||||
final byte[] lenBytes = new byte[4];
|
final byte[] lenBytes = new byte[4];
|
||||||
@ -66,6 +63,12 @@ public class Receiver extends Thread {
|
|||||||
|
|
||||||
// Catch LV encoding errors
|
// Catch LV encoding errors
|
||||||
if (len != bytesRead) {
|
if (len != bytesRead) {
|
||||||
|
// Server has stopped sending, i.e. because he went offline
|
||||||
|
if (bytesRead == -1) {
|
||||||
|
isAlive = false;
|
||||||
|
logger.log(Level.INFO, "Lost connection to the server. Exiting receiver...");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
logger.log(Level.WARNING,
|
logger.log(Level.WARNING,
|
||||||
String.format("LV encoding violated: expected %d bytes, received %d bytes. Discarding object...", len, bytesRead));
|
String.format("LV encoding violated: expected %d bytes, received %d bytes. Discarding object...", len, bytesRead));
|
||||||
continue;
|
continue;
|
||||||
@ -78,17 +81,22 @@ public class Receiver extends Thread {
|
|||||||
// Get appropriate processor
|
// Get appropriate processor
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
final Consumer processor = processors.get(obj.getClass());
|
final Consumer processor = processors.get(obj.getClass());
|
||||||
if (processor == null)
|
|
||||||
logger.log(Level.WARNING, String.format("The received object has the %s for which no processor is defined.", obj.getClass()));
|
// Dispatch to the processor if present
|
||||||
else processor.accept(obj);
|
if (processor != null) processor.accept(obj);
|
||||||
|
// Dispatch to the event bus if the object is an event without a processor
|
||||||
|
else if (obj instanceof IEvent) eventBus.dispatch((IEvent) obj);
|
||||||
|
// Notify if no processor could be located
|
||||||
|
else logger.log(Level.WARNING,
|
||||||
|
String.format("The received object has the %s for which no processor is defined.", obj.getClass()));
|
||||||
}
|
}
|
||||||
} catch (final SocketException e) {
|
} catch (final SocketException | EOFException e) {
|
||||||
// Connection probably closed by client.
|
// Connection probably closed by client.
|
||||||
|
logger.log(Level.INFO, "Exiting receiver...");
|
||||||
return;
|
return;
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
logger.log(Level.SEVERE, "Error on receiver thread", e);
|
logger.log(Level.SEVERE, "Error on receiver thread", e);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
package envoy.client.net;
|
package envoy.client.net;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.util.logging.*;
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import envoy.client.data.Cache;
|
import envoy.client.data.*;
|
||||||
import envoy.client.data.LocalDB;
|
|
||||||
import envoy.data.Message;
|
import envoy.data.Message;
|
||||||
import envoy.event.MessageStatusChange;
|
import envoy.event.MessageStatusChange;
|
||||||
import envoy.util.EnvoyLog;
|
import envoy.util.EnvoyLog;
|
||||||
@ -14,15 +11,11 @@ import envoy.util.EnvoyLog;
|
|||||||
* Implements methods to send {@link Message}s and
|
* Implements methods to send {@link Message}s and
|
||||||
* {@link MessageStatusChange}s to the server or cache them inside a
|
* {@link MessageStatusChange}s to the server or cache them inside a
|
||||||
* {@link LocalDB} depending on the online status.
|
* {@link LocalDB} depending on the online status.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>WriteProxy.java</strong><br>
|
|
||||||
* Created: <strong>6 Feb 2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public class WriteProxy {
|
public final class WriteProxy {
|
||||||
|
|
||||||
private final Client client;
|
private final Client client;
|
||||||
private final LocalDB localDB;
|
private final LocalDB localDB;
|
||||||
@ -33,10 +26,9 @@ public class WriteProxy {
|
|||||||
* Initializes a write proxy using a client and a local database. The
|
* Initializes a write proxy using a client and a local database. The
|
||||||
* corresponding cache processors are injected into the caches.
|
* corresponding cache processors are injected into the caches.
|
||||||
*
|
*
|
||||||
* @param client the client used to send messages and message status change
|
* @param client the client instance used to send messages and events if online
|
||||||
* events
|
* @param localDB the local database used to cache messages and events if
|
||||||
* @param localDB the local database used to cache messages and message status
|
* offline
|
||||||
* change events
|
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public WriteProxy(Client client, LocalDB localDB) {
|
public WriteProxy(Client client, LocalDB localDB) {
|
||||||
@ -45,20 +37,12 @@ public class WriteProxy {
|
|||||||
|
|
||||||
// Initialize cache processors for messages and message status change events
|
// Initialize cache processors for messages and message status change events
|
||||||
localDB.getCacheMap().get(Message.class).setProcessor(msg -> {
|
localDB.getCacheMap().get(Message.class).setProcessor(msg -> {
|
||||||
try {
|
logger.log(Level.FINER, "Sending cached " + msg);
|
||||||
logger.log(Level.FINER, "Sending cached " + msg);
|
client.sendMessage(msg);
|
||||||
client.sendMessage(msg);
|
|
||||||
} catch (final IOException e) {
|
|
||||||
logger.log(Level.SEVERE, "Could not send cached message: ", e);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
localDB.getCacheMap().get(MessageStatusChange.class).setProcessor(evt -> {
|
localDB.getCacheMap().get(MessageStatusChange.class).setProcessor(evt -> {
|
||||||
logger.log(Level.FINER, "Sending cached " + evt);
|
logger.log(Level.FINER, "Sending cached " + evt);
|
||||||
try {
|
client.send(evt);
|
||||||
client.sendEvent(evt);
|
|
||||||
} catch (final IOException e) {
|
|
||||||
logger.log(Level.SEVERE, "Could not send cached message status change event: ", e);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,19 +52,16 @@ public class WriteProxy {
|
|||||||
*
|
*
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public void flushCache() {
|
public void flushCache() { localDB.getCacheMap().getMap().values().forEach(Cache::relay); }
|
||||||
localDB.getCacheMap().getMap().values().forEach(Cache::relay);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delivers a message to the server if online. Otherwise the message is cached
|
* Delivers a message to the server if online. Otherwise the message is cached
|
||||||
* inside the local database.
|
* inside the local database.
|
||||||
*
|
*
|
||||||
* @param message the message to send
|
* @param message the message to send
|
||||||
* @throws IOException if the message could not be sent
|
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public void writeMessage(Message message) throws IOException {
|
public void writeMessage(Message message) {
|
||||||
if (client.isOnline()) client.sendMessage(message);
|
if (client.isOnline()) client.sendMessage(message);
|
||||||
else localDB.getCacheMap().getApplicable(Message.class).accept(message);
|
else localDB.getCacheMap().getApplicable(Message.class).accept(message);
|
||||||
}
|
}
|
||||||
@ -90,11 +71,10 @@ public class WriteProxy {
|
|||||||
* event is cached inside the local database.
|
* event is cached inside the local database.
|
||||||
*
|
*
|
||||||
* @param evt the event to send
|
* @param evt the event to send
|
||||||
* @throws IOException if the event could not be sent
|
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public void writeMessageStatusChange(MessageStatusChange evt) throws IOException {
|
public void writeMessageStatusChange(MessageStatusChange evt) {
|
||||||
if (client.isOnline()) client.sendEvent(evt);
|
if (client.isOnline()) client.send(evt);
|
||||||
else localDB.getCacheMap().getApplicable(MessageStatusChange.class).accept(evt);
|
else localDB.getCacheMap().getApplicable(MessageStatusChange.class).accept(evt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,169 +0,0 @@
|
|||||||
package envoy.client.ui;
|
|
||||||
|
|
||||||
import javafx.beans.property.BooleanProperty;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.StringProperty;
|
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.event.EventHandler;
|
|
||||||
import javafx.scene.control.*;
|
|
||||||
import javafx.scene.image.ImageView;
|
|
||||||
import javafx.scene.layout.Background;
|
|
||||||
import javafx.scene.layout.ColumnConstraints;
|
|
||||||
import javafx.scene.layout.GridPane;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class offers a text field that is automatically equipped with a clear
|
|
||||||
* button.
|
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>ClearableTextField.java</strong><br>
|
|
||||||
* Created: <strong>25.06.2020</strong><br>
|
|
||||||
*
|
|
||||||
* @author Leon Hofmeister
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public class ClearableTextField extends GridPane {
|
|
||||||
|
|
||||||
private final TextField textField;
|
|
||||||
|
|
||||||
private final Button clearButton;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code ClearableTextField} with no initial text and icon
|
|
||||||
* size 16.
|
|
||||||
*
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public ClearableTextField() { this("", 16); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code ClearableTextField} with initial text and a
|
|
||||||
* predetermined icon size.
|
|
||||||
*
|
|
||||||
* @param text the text that should be displayed by default
|
|
||||||
* @param size the size of the icon
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public ClearableTextField(String text, int size) {
|
|
||||||
// initializing the textField and the button
|
|
||||||
textField = new TextField(text);
|
|
||||||
clearButton = new Button("", new ImageView(IconUtil.loadIconThemeSensitive("clear_button", size)));
|
|
||||||
clearButton.setOnAction(e -> textField.clear());
|
|
||||||
clearButton.setFocusTraversable(false);
|
|
||||||
clearButton.getStyleClass().clear();
|
|
||||||
clearButton.setBackground(Background.EMPTY);
|
|
||||||
// Adding the two elements to the GridPane
|
|
||||||
add(textField, 0, 0, 2, 1);
|
|
||||||
add(clearButton, 1, 0, 1, 1);
|
|
||||||
// Setting the percent - widths of the two columns.
|
|
||||||
// Used to locate the button on the right.
|
|
||||||
final var columnConstraints = new ColumnConstraints();
|
|
||||||
columnConstraints.setPercentWidth(90);
|
|
||||||
getColumnConstraints().add(columnConstraints);
|
|
||||||
final var columnConstraints2 = new ColumnConstraints();
|
|
||||||
columnConstraints2.setPercentWidth(10);
|
|
||||||
getColumnConstraints().add(columnConstraints2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the underlying {@code textField}
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public TextField getTextField() { return textField; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method offers the freedom to perform custom actions when the
|
|
||||||
* {@code clearButton} has been pressed.
|
|
||||||
* <p>
|
|
||||||
* The default is
|
|
||||||
* <b><code> e -> {clearableTextField.getTextField().clear();}</code></b>
|
|
||||||
*
|
|
||||||
* @param onClearButtonAction the action that should be performed
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public void setClearButtonListener(EventHandler<ActionEvent> onClearButtonAction) { clearButton.setOnAction(onClearButtonAction); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the current property of the prompt text
|
|
||||||
* @see javafx.scene.control.TextInputControl#promptTextProperty()
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public final StringProperty promptTextProperty() { return textField.promptTextProperty(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the current prompt text
|
|
||||||
* @see javafx.scene.control.TextInputControl#getPromptText()
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public final String getPromptText() { return textField.getPromptText(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param value the prompt text to display
|
|
||||||
* @see javafx.scene.control.TextInputControl#setPromptText(java.lang.String)
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public final void setPromptText(String value) { textField.setPromptText(value); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the current property of the tooltip
|
|
||||||
* @see javafx.scene.control.Control#tooltipProperty()
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public final ObjectProperty<Tooltip> tooltipProperty() { return textField.tooltipProperty(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param value the new tooltip
|
|
||||||
* @see javafx.scene.control.Control#setTooltip(javafx.scene.control.Tooltip)
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public final void setTooltip(Tooltip value) { textField.setTooltip(value); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the current tooltip
|
|
||||||
* @see javafx.scene.control.Control#getTooltip()
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public final Tooltip getTooltip() { return textField.getTooltip(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the current property of the context menu
|
|
||||||
* @see javafx.scene.control.Control#contextMenuProperty()
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public final ObjectProperty<ContextMenu> contextMenuProperty() { return textField.contextMenuProperty(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param value the new context menu
|
|
||||||
* @see javafx.scene.control.Control#setContextMenu(javafx.scene.control.ContextMenu)
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public final void setContextMenu(ContextMenu value) { textField.setContextMenu(value); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the current context menu
|
|
||||||
* @see javafx.scene.control.Control#getContextMenu()
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public final ContextMenu getContextMenu() { return textField.getContextMenu(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param value whether this ClearableTextField should be editable
|
|
||||||
* @see javafx.scene.control.TextInputControl#setEditable(boolean)
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public final void setEditable(boolean value) { textField.setEditable(value); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the current property whether this ClearableTextField is editable
|
|
||||||
* @see javafx.scene.control.TextInputControl#editableProperty()
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public final BooleanProperty editableProperty() { return textField.editableProperty(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return whether this {@code ClearableTextField} is editable
|
|
||||||
* @see javafx.scene.control.TextInputControl#isEditable()
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public final boolean isEditable() { return textField.isEditable(); }
|
|
||||||
}
|
|
@ -3,10 +3,6 @@ package envoy.client.ui;
|
|||||||
/**
|
/**
|
||||||
* This interface defines an action that should be performed when a scene gets
|
* This interface defines an action that should be performed when a scene gets
|
||||||
* restored from the scene stack in {@link SceneContext}.
|
* restored from the scene stack in {@link SceneContext}.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>Restorable.java</strong><br>
|
|
||||||
* Created: <strong>03.07.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Leon Hofmeister
|
* @author Leon Hofmeister
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
|
@ -4,16 +4,19 @@ import java.io.IOException;
|
|||||||
import java.util.Stack;
|
import java.util.Stack;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
import javafx.scene.Parent;
|
import javafx.scene.*;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.input.*;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
import envoy.client.data.Settings;
|
import envoy.client.data.Settings;
|
||||||
import envoy.client.event.ThemeChangeEvent;
|
import envoy.client.event.*;
|
||||||
import envoy.event.EventBus;
|
import envoy.client.helper.ShutdownHelper;
|
||||||
import envoy.util.EnvoyLog;
|
import envoy.util.EnvoyLog;
|
||||||
|
|
||||||
|
import dev.kske.eventbus.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages a stack of scenes. The most recently added scene is displayed inside
|
* Manages a stack of scenes. The most recently added scene is displayed inside
|
||||||
* a stage. When a scene is removed from the stack, its predecessor is
|
* a stage. When a scene is removed from the stack, its predecessor is
|
||||||
@ -21,15 +24,11 @@ import envoy.util.EnvoyLog;
|
|||||||
* <p>
|
* <p>
|
||||||
* When a scene is loaded, the style sheet for the current theme is applied to
|
* When a scene is loaded, the style sheet for the current theme is applied to
|
||||||
* it.
|
* it.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>SceneContext.java</strong><br>
|
|
||||||
* Created: <strong>06.06.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public final class SceneContext {
|
public final class SceneContext implements EventListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains information about different scenes and their FXML resource files.
|
* Contains information about different scenes and their FXML resource files.
|
||||||
@ -53,33 +52,12 @@ public final class SceneContext {
|
|||||||
*/
|
*/
|
||||||
SETTINGS_SCENE("/fxml/SettingsScene.fxml"),
|
SETTINGS_SCENE("/fxml/SettingsScene.fxml"),
|
||||||
|
|
||||||
/**
|
|
||||||
* The scene in which the contact search screen is displayed.
|
|
||||||
*
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
CONTACT_SEARCH_SCENE("/fxml/ContactSearchScene.fxml"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The scene in which the group creation screen is displayed.
|
|
||||||
*
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
GROUP_CREATION_SCENE("/fxml/GroupCreationScene.fxml"),
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The scene in which the login screen is displayed.
|
* The scene in which the login screen is displayed.
|
||||||
*
|
*
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
LOGIN_SCENE("/fxml/LoginScene.fxml"),
|
LOGIN_SCENE("/fxml/LoginScene.fxml");
|
||||||
|
|
||||||
/**
|
|
||||||
* The scene in which the info screen is displayed.
|
|
||||||
*
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
MESSAGE_INFO_SCENE("/fxml/MessageInfoScene.fxml");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The path to the FXML resource.
|
* The path to the FXML resource.
|
||||||
@ -104,7 +82,7 @@ public final class SceneContext {
|
|||||||
*/
|
*/
|
||||||
public SceneContext(Stage stage) {
|
public SceneContext(Stage stage) {
|
||||||
this.stage = stage;
|
this.stage = stage;
|
||||||
EventBus.getInstance().register(ThemeChangeEvent.class, theme -> applyCSS());
|
EventBus.getInstance().registerListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -115,6 +93,7 @@ public final class SceneContext {
|
|||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public void load(SceneInfo sceneInfo) {
|
public void load(SceneInfo sceneInfo) {
|
||||||
|
EnvoyLog.getLogger(SceneContext.class).log(Level.FINER, "Loading scene " + sceneInfo);
|
||||||
loader.setRoot(null);
|
loader.setRoot(null);
|
||||||
loader.setController(null);
|
loader.setController(null);
|
||||||
|
|
||||||
@ -125,8 +104,25 @@ public final class SceneContext {
|
|||||||
|
|
||||||
sceneStack.push(scene);
|
sceneStack.push(scene);
|
||||||
stage.setScene(scene);
|
stage.setScene(scene);
|
||||||
applyCSS();
|
|
||||||
|
// Add the option to exit Linux-like with "Control" + "Q"
|
||||||
|
scene.getAccelerators().put(new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN), ShutdownHelper::exit);
|
||||||
|
|
||||||
|
// Add the option to logout using "Control"+"Shift"+"L" if not in login scene
|
||||||
|
if (sceneInfo != SceneInfo.LOGIN_SCENE) scene.getAccelerators()
|
||||||
|
.put(new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN), ShutdownHelper::logout);
|
||||||
|
|
||||||
|
// Add the option to open the settings scene with "Control"+"S", if being in
|
||||||
|
// chat scene
|
||||||
|
if (sceneInfo.equals(SceneInfo.CHAT_SCENE))
|
||||||
|
scene.getAccelerators().put(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN), () -> load(SceneInfo.SETTINGS_SCENE));
|
||||||
|
|
||||||
|
// The LoginScene is the only scene not intended to be resized
|
||||||
|
// As strange as it seems, this is needed as otherwise the LoginScene won't be
|
||||||
|
// displayed on some OS (...Debian...)
|
||||||
stage.sizeToScene();
|
stage.sizeToScene();
|
||||||
|
Platform.runLater(() -> stage.setResizable(sceneInfo != SceneInfo.LOGIN_SCENE));
|
||||||
|
applyCSS();
|
||||||
stage.show();
|
stage.show();
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
EnvoyLog.getLogger(SceneContext.class).log(Level.SEVERE, String.format("Could not load scene for %s: ", sceneInfo), e);
|
EnvoyLog.getLogger(SceneContext.class).log(Level.SEVERE, String.format("Could not load scene for %s: ", sceneInfo), e);
|
||||||
@ -140,8 +136,12 @@ public final class SceneContext {
|
|||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public void pop() {
|
public void pop() {
|
||||||
|
|
||||||
|
// Pop scene and controller
|
||||||
sceneStack.pop();
|
sceneStack.pop();
|
||||||
controllerStack.pop();
|
controllerStack.pop();
|
||||||
|
|
||||||
|
// Apply new scene if present
|
||||||
if (!sceneStack.isEmpty()) {
|
if (!sceneStack.isEmpty()) {
|
||||||
final var newScene = sceneStack.peek();
|
final var newScene = sceneStack.peek();
|
||||||
stage.setScene(newScene);
|
stage.setScene(newScene);
|
||||||
@ -164,6 +164,15 @@ public final class SceneContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Event(eventType = Logout.class, priority = 150)
|
||||||
|
private void onLogout() {
|
||||||
|
sceneStack.clear();
|
||||||
|
controllerStack.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Event(priority = 150, eventType = ThemeChangeEvent.class)
|
||||||
|
private void onThemeChange() { applyCSS(); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param <T> the type of the controller
|
* @param <T> the type of the controller
|
||||||
* @return the controller used by the current scene
|
* @return the controller used by the current scene
|
||||||
@ -176,4 +185,10 @@ public final class SceneContext {
|
|||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public Stage getStage() { return stage; }
|
public Stage getStage() { return stage; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return whether the scene stack is empty
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public boolean isEmpty() { return sceneStack.isEmpty(); }
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
package envoy.client.ui;
|
package envoy.client.ui;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.*;
|
||||||
import java.io.IOException;
|
import java.time.Instant;
|
||||||
import java.util.Properties;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.*;
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
@ -12,22 +11,19 @@ import javafx.scene.control.Alert.AlertType;
|
|||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
import envoy.client.data.*;
|
import envoy.client.data.*;
|
||||||
|
import envoy.client.helper.ShutdownHelper;
|
||||||
import envoy.client.net.Client;
|
import envoy.client.net.Client;
|
||||||
import envoy.client.ui.SceneContext.SceneInfo;
|
import envoy.client.ui.SceneContext.SceneInfo;
|
||||||
import envoy.client.ui.controller.LoginScene;
|
import envoy.client.ui.controller.LoginScene;
|
||||||
import envoy.data.GroupMessage;
|
import envoy.client.util.IconUtil;
|
||||||
import envoy.data.Message;
|
import envoy.data.*;
|
||||||
import envoy.event.GroupMessageStatusChange;
|
import envoy.data.User.UserStatus;
|
||||||
import envoy.event.MessageStatusChange;
|
import envoy.event.*;
|
||||||
import envoy.exception.EnvoyException;
|
import envoy.exception.EnvoyException;
|
||||||
import envoy.util.EnvoyLog;
|
import envoy.util.EnvoyLog;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles application startup and shutdown.
|
* Handles application startup.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>Startup.java</strong><br>
|
|
||||||
* Created: <strong>26.03.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @author Maximilian Käfer
|
* @author Maximilian Käfer
|
||||||
@ -42,9 +38,10 @@ public final class Startup extends Application {
|
|||||||
*/
|
*/
|
||||||
public static final String VERSION = "0.1-beta";
|
public static final String VERSION = "0.1-beta";
|
||||||
|
|
||||||
private LocalDB localDB;
|
private static LocalDB localDB;
|
||||||
private Client client;
|
|
||||||
|
|
||||||
|
private static final Context context = Context.getInstance();
|
||||||
|
private static final Client client = context.getClient();
|
||||||
private static final ClientConfig config = ClientConfig.getInstance();
|
private static final ClientConfig config = ClientConfig.getInstance();
|
||||||
private static final Logger logger = EnvoyLog.getLogger(Startup.class);
|
private static final Logger logger = EnvoyLog.getLogger(Startup.class);
|
||||||
|
|
||||||
@ -56,80 +53,172 @@ public final class Startup extends Application {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void start(Stage stage) throws Exception {
|
public void start(Stage stage) throws Exception {
|
||||||
|
|
||||||
|
// Initialize config and logger
|
||||||
try {
|
try {
|
||||||
// Load the configuration from client.properties first
|
config.loadAll(Startup.class, "client.properties", getParameters().getRaw().toArray(new String[0]));
|
||||||
final Properties properties = new Properties();
|
EnvoyLog.initialize(config);
|
||||||
properties.load(Startup.class.getClassLoader().getResourceAsStream("client.properties"));
|
} catch (final IllegalStateException e) {
|
||||||
config.load(properties);
|
|
||||||
|
|
||||||
// Override configuration values with command line arguments
|
|
||||||
final String[] args = getParameters().getRaw().toArray(new String[0]);
|
|
||||||
if (args.length > 0) config.load(args);
|
|
||||||
|
|
||||||
// Check if all mandatory configuration values have been initialized
|
|
||||||
if (!config.isInitialized()) throw new EnvoyException("Configuration is not fully initialized");
|
|
||||||
} catch (final Exception e) {
|
|
||||||
new Alert(AlertType.ERROR, "Error loading configuration values:\n" + e);
|
new Alert(AlertType.ERROR, "Error loading configuration values:\n" + e);
|
||||||
logger.log(Level.SEVERE, "Error loading configuration values: ", e);
|
logger.log(Level.SEVERE, "Error loading configuration values: ", e);
|
||||||
e.printStackTrace();
|
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup logger for the envoy package
|
|
||||||
EnvoyLog.initialize(config);
|
|
||||||
EnvoyLog.attach("envoy");
|
|
||||||
EnvoyLog.setFileLevelBarrier(config.getFileLevelBarrier());
|
|
||||||
EnvoyLog.setConsoleLevelBarrier(config.getConsoleLevelBarrier());
|
|
||||||
|
|
||||||
logger.log(Level.INFO, "Envoy starting...");
|
logger.log(Level.INFO, "Envoy starting...");
|
||||||
|
|
||||||
// Initialize the local database
|
// Initialize the local database
|
||||||
if (config.isIgnoreLocalDB()) {
|
try {
|
||||||
localDB = new TransientLocalDB();
|
final var localDBFile = new File(config.getHomeDirectory(), config.getServer());
|
||||||
new Alert(AlertType.WARNING, "Ignoring local database.\nMessages will not be saved!").showAndWait();
|
logger.info("Initializing LocalDB at " + localDBFile);
|
||||||
} else try {
|
localDB = new LocalDB(localDBFile);
|
||||||
localDB = new PersistentLocalDB(new File(config.getHomeDirectory(), config.getLocalDB().getPath()));
|
} catch (IOException | EnvoyException e) {
|
||||||
} catch (final IOException e3) {
|
logger.log(Level.SEVERE, "Could not initialize local database: ", e);
|
||||||
logger.log(Level.SEVERE, "Could not initialize local database: ", e3);
|
new Alert(AlertType.ERROR, "Could not initialize local database!\n" + e).showAndWait();
|
||||||
new Alert(AlertType.ERROR, "Could not initialize local database!\n" + e3).showAndWait();
|
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize client and unread message cache
|
// Prepare handshake
|
||||||
client = new Client();
|
context.setLocalDB(localDB);
|
||||||
|
|
||||||
|
// Configure stage
|
||||||
|
stage.setTitle("Envoy");
|
||||||
|
stage.getIcons().add(IconUtil.loadIcon("envoy_logo"));
|
||||||
|
|
||||||
|
// Create scene context
|
||||||
|
final var sceneContext = new SceneContext(stage);
|
||||||
|
context.setSceneContext(sceneContext);
|
||||||
|
|
||||||
|
// Authenticate with token if present
|
||||||
|
if (localDB.getAuthToken() != null) {
|
||||||
|
logger.info("Attempting authentication with token...");
|
||||||
|
localDB.loadUserData();
|
||||||
|
if (!performHandshake(
|
||||||
|
LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(), VERSION, localDB.getLastSync())))
|
||||||
|
sceneContext.load(SceneInfo.LOGIN_SCENE);
|
||||||
|
} else
|
||||||
|
// Load login scene
|
||||||
|
sceneContext.load(SceneInfo.LOGIN_SCENE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to perform a Handshake with the server.
|
||||||
|
*
|
||||||
|
* @param credentials the credentials to use for the handshake
|
||||||
|
* @return whether the handshake was successful or offline mode could be entered
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public static boolean performHandshake(LoginCredentials credentials) {
|
||||||
final var cacheMap = new CacheMap();
|
final var cacheMap = new CacheMap();
|
||||||
cacheMap.put(Message.class, new Cache<Message>());
|
cacheMap.put(Message.class, new Cache<Message>());
|
||||||
cacheMap.put(GroupMessage.class, new Cache<GroupMessage>());
|
cacheMap.put(GroupMessage.class, new Cache<GroupMessage>());
|
||||||
cacheMap.put(MessageStatusChange.class, new Cache<MessageStatusChange>());
|
cacheMap.put(MessageStatusChange.class, new Cache<MessageStatusChange>());
|
||||||
cacheMap.put(GroupMessageStatusChange.class, new Cache<GroupMessageStatusChange>());
|
cacheMap.put(GroupMessageStatusChange.class, new Cache<GroupMessageStatusChange>());
|
||||||
|
try {
|
||||||
stage.setTitle("Envoy");
|
client.performHandshake(credentials, cacheMap);
|
||||||
stage.getIcons().add(IconUtil.loadIcon("envoy_logo"));
|
if (client.isOnline()) {
|
||||||
|
loadChatScene();
|
||||||
final var sceneContext = new SceneContext(stage);
|
client.initReceiver(localDB, cacheMap);
|
||||||
sceneContext.load(SceneInfo.LOGIN_SCENE);
|
return true;
|
||||||
sceneContext.<LoginScene>getController().initializeData(client, localDB, cacheMap, sceneContext);
|
} else return false;
|
||||||
|
} catch (IOException | InterruptedException | TimeoutException e) {
|
||||||
|
logger.log(Level.INFO, "Could not connect to server. Entering offline mode...");
|
||||||
|
return attemptOfflineMode(credentials.getIdentifier());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the client connection and saves the local database and settings.
|
* Attempts to load {@link envoy.client.ui.controller.ChatScene} in offline mode
|
||||||
|
* for a given user.
|
||||||
*
|
*
|
||||||
* @since Envoy Client v0.1-beta
|
* @param identifier the identifier of the user - currently his username
|
||||||
|
* @return whether the offline mode could be entered
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
*/
|
*/
|
||||||
@Override
|
public static boolean attemptOfflineMode(String identifier) {
|
||||||
public void stop() {
|
|
||||||
try {
|
try {
|
||||||
logger.log(Level.INFO, "Closing connection...");
|
// Try entering offline mode
|
||||||
client.close();
|
final User clientUser = localDB.getUsers().get(identifier);
|
||||||
|
if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
|
||||||
logger.log(Level.INFO, "Saving local database and settings...");
|
client.setSender(clientUser);
|
||||||
localDB.save();
|
loadChatScene();
|
||||||
Settings.getInstance().save();
|
return true;
|
||||||
logger.log(Level.INFO, "Envoy was terminated by its user");
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
logger.log(Level.SEVERE, "Unable to save local files: ", e);
|
new Alert(AlertType.ERROR, "Client error: " + e).showAndWait();
|
||||||
|
logger.log(Level.SEVERE, "Offline mode could not be loaded: ", e);
|
||||||
|
System.exit(1);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the last known time a user has been online.
|
||||||
|
*
|
||||||
|
* @param identifier the identifier of this user - currently his name
|
||||||
|
* @return the last {@code Instant} at which he has been online
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public static Instant loadLastSync(String identifier) {
|
||||||
|
try {
|
||||||
|
localDB.setUser(localDB.getUsers().get(identifier));
|
||||||
|
localDB.loadUserData();
|
||||||
|
} catch (final Exception e) {
|
||||||
|
// User storage empty, wrong user name etc. -> default lastSync
|
||||||
|
}
|
||||||
|
return localDB.getLastSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void loadChatScene() {
|
||||||
|
|
||||||
|
// Set client user in local database
|
||||||
|
localDB.setUser(client.getSender());
|
||||||
|
|
||||||
|
// Initialize chats in local database
|
||||||
|
try {
|
||||||
|
localDB.loadUserData();
|
||||||
|
} catch (final FileNotFoundException e) {
|
||||||
|
// The local database file has not yet been created, probably first login
|
||||||
|
} catch (final Exception e) {
|
||||||
|
new Alert(AlertType.ERROR, "Error while loading local database: " + e + "\nChats will not be stored locally.").showAndWait();
|
||||||
|
logger.log(Level.WARNING, "Could not load local database: ", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.initWriteProxy();
|
||||||
|
|
||||||
|
if (client.isOnline()) context.getWriteProxy().flushCache();
|
||||||
|
else
|
||||||
|
// Set all contacts to offline mode
|
||||||
|
localDB.getChats()
|
||||||
|
.stream()
|
||||||
|
.map(Chat::getRecipient)
|
||||||
|
.filter(User.class::isInstance)
|
||||||
|
.map(User.class::cast)
|
||||||
|
.forEach(u -> u.setStatus(UserStatus.OFFLINE));
|
||||||
|
|
||||||
|
final var stage = context.getStage();
|
||||||
|
|
||||||
|
// Pop LoginScene if present
|
||||||
|
if (!context.getSceneContext().isEmpty()) context.getSceneContext().pop();
|
||||||
|
|
||||||
|
// Load ChatScene
|
||||||
|
stage.setMinHeight(400);
|
||||||
|
stage.setMinWidth(843);
|
||||||
|
context.getSceneContext().load(SceneContext.SceneInfo.CHAT_SCENE);
|
||||||
|
stage.centerOnScreen();
|
||||||
|
|
||||||
|
if (StatusTrayIcon.isSupported()) {
|
||||||
|
|
||||||
|
// Exit or minimize the stage when a close request occurs
|
||||||
|
stage.setOnCloseRequest(e -> { ShutdownHelper.exit(); if (Settings.getInstance().isHideOnClose()) e.consume(); });
|
||||||
|
|
||||||
|
// Initialize status tray icon
|
||||||
|
final var trayIcon = new StatusTrayIcon(stage);
|
||||||
|
Settings.getInstance().getItems().get("hideOnClose").setChangeHandler(c -> {
|
||||||
|
if ((Boolean) c) trayIcon.show();
|
||||||
|
else trayIcon.hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start auto save thread
|
||||||
|
localDB.initAutoSave();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,25 +2,22 @@ package envoy.client.ui;
|
|||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.TrayIcon.MessageType;
|
import java.awt.TrayIcon.MessageType;
|
||||||
import java.awt.event.WindowAdapter;
|
|
||||||
import java.awt.event.WindowEvent;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
|
|
||||||
import envoy.client.event.MessageCreationEvent;
|
import javafx.application.Platform;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
import envoy.client.helper.ShutdownHelper;
|
||||||
|
import envoy.client.util.IconUtil;
|
||||||
import envoy.data.Message;
|
import envoy.data.Message;
|
||||||
import envoy.event.EventBus;
|
|
||||||
import envoy.exception.EnvoyException;
|
import dev.kske.eventbus.*;
|
||||||
import envoy.util.EnvoyLog;
|
import dev.kske.eventbus.Event;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>StatusTrayIcon.java</strong><br>
|
|
||||||
* Created: <strong>3 Dec 2019</strong><br>
|
|
||||||
*
|
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.2-alpha
|
* @since Envoy Client v0.2-alpha
|
||||||
*/
|
*/
|
||||||
public class StatusTrayIcon {
|
public final class StatusTrayIcon implements EventListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link TrayIcon} provided by the System Tray API for controlling the
|
* The {@link TrayIcon} provided by the System Tray API for controlling the
|
||||||
@ -33,68 +30,68 @@ public class StatusTrayIcon {
|
|||||||
* A received {@link Message} is only displayed as a system tray notification if
|
* A received {@link Message} is only displayed as a system tray notification if
|
||||||
* this variable is set to {@code true}.
|
* this variable is set to {@code true}.
|
||||||
*/
|
*/
|
||||||
private boolean displayMessages = false;
|
private boolean displayMessages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {@code true} if the status tray icon is supported on this platform
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public static boolean isSupported() { return SystemTray.isSupported(); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up
|
* Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up
|
||||||
* menu.
|
* menu.
|
||||||
*
|
*
|
||||||
* @param focusTarget the {@link Window} which focus determines if message
|
* @param stage the stage whose focus determines if message
|
||||||
* notifications are displayed
|
* notifications are displayed
|
||||||
* @throws EnvoyException if the currently used OS does not support the System
|
* @since Envoy Client v0.2-beta
|
||||||
* Tray API
|
|
||||||
* @since Envoy Client v0.2-alpha
|
|
||||||
*/
|
*/
|
||||||
public StatusTrayIcon(Window focusTarget) throws EnvoyException {
|
public StatusTrayIcon(Stage stage) {
|
||||||
if (!SystemTray.isSupported()) throw new EnvoyException("The Envoy tray icon is not supported.");
|
trayIcon = new TrayIcon(IconUtil.loadAWTCompatible("/icons/envoy_logo.png"), "Envoy");
|
||||||
|
|
||||||
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
|
|
||||||
final Image img = Toolkit.getDefaultToolkit().createImage(loader.getResource("envoy_logo.png"));
|
|
||||||
trayIcon = new TrayIcon(img, "Envoy Client");
|
|
||||||
trayIcon.setImageAutoSize(true);
|
trayIcon.setImageAutoSize(true);
|
||||||
trayIcon.setToolTip("You are notified if you have unread messages.");
|
trayIcon.setToolTip("You are notified if you have unread messages.");
|
||||||
|
|
||||||
final PopupMenu popup = new PopupMenu();
|
final PopupMenu popup = new PopupMenu();
|
||||||
|
|
||||||
final MenuItem exitMenuItem = new MenuItem("Exit");
|
final MenuItem exitMenuItem = new MenuItem("Exit");
|
||||||
exitMenuItem.addActionListener(evt -> System.exit(0));
|
exitMenuItem.addActionListener(evt -> ShutdownHelper.exit());
|
||||||
popup.add(exitMenuItem);
|
popup.add(exitMenuItem);
|
||||||
|
|
||||||
trayIcon.setPopupMenu(popup);
|
trayIcon.setPopupMenu(popup);
|
||||||
|
|
||||||
// Only display messages if the chat window is not focused
|
// Only display messages if the stage is not focused
|
||||||
focusTarget.addWindowFocusListener(new WindowAdapter() {
|
stage.focusedProperty().addListener((ov, onHidden, onShown) -> displayMessages = !ov.getValue());
|
||||||
|
|
||||||
@Override
|
|
||||||
public void windowGainedFocus(WindowEvent e) { displayMessages = false; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void windowLostFocus(WindowEvent e) { displayMessages = true; }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show the window if the user clicks on the icon
|
// Show the window if the user clicks on the icon
|
||||||
trayIcon.addActionListener(evt -> { focusTarget.setVisible(true); focusTarget.requestFocus(); });
|
trayIcon.addActionListener(evt -> Platform.runLater(() -> { stage.setIconified(false); stage.toFront(); stage.requestFocus(); }));
|
||||||
|
|
||||||
// Start processing message events
|
// Start processing message events
|
||||||
// TODO: Handle other message types
|
EventBus.getInstance().registerListener(this);
|
||||||
EventBus.getInstance()
|
|
||||||
.register(MessageCreationEvent.class,
|
|
||||||
evt -> { if (displayMessages) trayIcon.displayMessage("New message received", evt.get().getText(), MessageType.INFO); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes this {@link StatusTrayIcon} appear in the system tray.
|
* Makes the icon appear in the system tray.
|
||||||
*
|
*
|
||||||
* @throws EnvoyException if the status icon could not be attaches to the system
|
|
||||||
* tray for system-internal reasons
|
|
||||||
* @since Envoy Client v0.2-alpha
|
* @since Envoy Client v0.2-alpha
|
||||||
*/
|
*/
|
||||||
public void show() throws EnvoyException {
|
public void show() {
|
||||||
try {
|
try {
|
||||||
SystemTray.getSystemTray().add(trayIcon);
|
SystemTray.getSystemTray().add(trayIcon);
|
||||||
} catch (final AWTException e) {
|
} catch (final AWTException e) {}
|
||||||
EnvoyLog.getLogger(StatusTrayIcon.class).log(Level.INFO, "Could not display StatusTrayIcon: ", e);
|
}
|
||||||
throw new EnvoyException("Could not attach Envoy tray icon to system tray.", e);
|
|
||||||
}
|
/**
|
||||||
|
* Removes the icon from the system tray.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public void hide() { SystemTray.getSystemTray().remove(trayIcon); }
|
||||||
|
|
||||||
|
@Event
|
||||||
|
private void onMessage(Message message) {
|
||||||
|
if (displayMessages) trayIcon.displayMessage(
|
||||||
|
message.hasAttachment() ? "New " + message.getAttachment().getType().toString().toLowerCase() + " message received" : "New message received",
|
||||||
|
message.getText(),
|
||||||
|
MessageType.INFO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
package envoy.client.ui;
|
package envoy.client.ui.control;
|
||||||
|
|
||||||
import java.util.logging.Level;
|
import java.util.logging.*;
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.Alert.AlertType;
|
import javafx.scene.control.Alert.AlertType;
|
||||||
import javafx.scene.control.Button;
|
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
|
|
||||||
import envoy.client.data.audio.AudioPlayer;
|
import envoy.client.data.audio.AudioPlayer;
|
||||||
@ -14,10 +12,6 @@ import envoy.util.EnvoyLog;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Enables the play back of audio clips through a button.
|
* Enables the play back of audio clips through a button.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>AudioControl.java</strong><br>
|
|
||||||
* Created: <strong>05.07.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
@ -0,0 +1,69 @@
|
|||||||
|
package envoy.client.ui.control;
|
||||||
|
|
||||||
|
import javafx.geometry.*;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.image.*;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
import javafx.scene.shape.Rectangle;
|
||||||
|
|
||||||
|
import envoy.client.data.*;
|
||||||
|
import envoy.client.util.IconUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a chat using a contact control for the recipient and a label for the
|
||||||
|
* unread message count.
|
||||||
|
*
|
||||||
|
* @see ContactControl
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public final class ChatControl extends HBox {
|
||||||
|
|
||||||
|
private static final Image userIcon = IconUtil.loadIconThemeSensitive("user_icon", 32),
|
||||||
|
groupIcon = IconUtil.loadIconThemeSensitive("group_icon", 32);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param chat the chat to display
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public ChatControl(Chat chat) {
|
||||||
|
setAlignment(Pos.CENTER_LEFT);
|
||||||
|
setPadding(new Insets(0, 0, 3, 0));
|
||||||
|
|
||||||
|
// Profile picture
|
||||||
|
ImageView contactProfilePic = new ImageView(chat instanceof GroupChat ? groupIcon : userIcon);
|
||||||
|
final var clip = new Rectangle();
|
||||||
|
clip.setWidth(32);
|
||||||
|
clip.setHeight(32);
|
||||||
|
clip.setArcHeight(32);
|
||||||
|
clip.setArcWidth(32);
|
||||||
|
contactProfilePic.setClip(clip);
|
||||||
|
getChildren().add(contactProfilePic);
|
||||||
|
|
||||||
|
// Spacing
|
||||||
|
final var leftSpacing = new Region();
|
||||||
|
leftSpacing.setPrefSize(8, 0);
|
||||||
|
leftSpacing.setMinSize(8, 0);
|
||||||
|
leftSpacing.setMaxSize(8, 0);
|
||||||
|
getChildren().add(leftSpacing);
|
||||||
|
|
||||||
|
// Contact control
|
||||||
|
getChildren().add(new ContactControl(chat.getRecipient()));
|
||||||
|
|
||||||
|
// Unread messages
|
||||||
|
if (chat.getUnreadAmount() != 0) {
|
||||||
|
final var spacing = new Region();
|
||||||
|
setHgrow(spacing, Priority.ALWAYS);
|
||||||
|
getChildren().add(spacing);
|
||||||
|
final var unreadMessagesLabel = new Label(Integer.toString(chat.getUnreadAmount()));
|
||||||
|
unreadMessagesLabel.setMinSize(15, 15);
|
||||||
|
final var vbox = new VBox();
|
||||||
|
vbox.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
unreadMessagesLabel.setAlignment(Pos.CENTER);
|
||||||
|
unreadMessagesLabel.getStyleClass().add("unread-messages-amount");
|
||||||
|
vbox.getChildren().add(unreadMessagesLabel);
|
||||||
|
getChildren().add(vbox);
|
||||||
|
}
|
||||||
|
getStyleClass().add("list-element");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package envoy.client.ui.control;
|
||||||
|
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
import envoy.data.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays information about a contact in two rows. The first row contains the
|
||||||
|
* name. The second row contains the online status (user) or the member count
|
||||||
|
* (group).
|
||||||
|
*
|
||||||
|
* @author Kai S. K. Engelbart
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public final class ContactControl extends VBox {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param contact the contact to display
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public ContactControl(Contact contact) {
|
||||||
|
|
||||||
|
// Name label
|
||||||
|
final var nameLabel = new Label(contact.getName());
|
||||||
|
getChildren().add(nameLabel);
|
||||||
|
|
||||||
|
// Online status (user) or member count (group)
|
||||||
|
if (contact instanceof User) {
|
||||||
|
final var status = ((User) contact).getStatus().toString();
|
||||||
|
final var statusLabel = new Label(status);
|
||||||
|
statusLabel.getStyleClass().add(status.toLowerCase());
|
||||||
|
getChildren().add(statusLabel);
|
||||||
|
} else {
|
||||||
|
getChildren().add(new Label(contact.getContacts().size() + " members"));
|
||||||
|
}
|
||||||
|
getStyleClass().add("list-element");
|
||||||
|
}
|
||||||
|
}
|
173
client/src/main/java/envoy/client/ui/control/MessageControl.java
Normal file
173
client/src/main/java/envoy/client/ui/control/MessageControl.java
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package envoy.client.ui.control;
|
||||||
|
|
||||||
|
import java.awt.Toolkit;
|
||||||
|
import java.awt.datatransfer.StringSelection;
|
||||||
|
import java.io.*;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.logging.*;
|
||||||
|
|
||||||
|
import javafx.geometry.*;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.*;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
import javafx.stage.FileChooser;
|
||||||
|
|
||||||
|
import envoy.client.data.*;
|
||||||
|
import envoy.client.ui.*;
|
||||||
|
import envoy.client.util.IconUtil;
|
||||||
|
import envoy.data.*;
|
||||||
|
import envoy.data.Message.MessageStatus;
|
||||||
|
import envoy.util.EnvoyLog;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class transforms a single {@link Message} into a UI component.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @author Maximilian Käfer
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public final class MessageControl extends Label {
|
||||||
|
|
||||||
|
private final boolean ownMessage;
|
||||||
|
|
||||||
|
private final LocalDB localDB = Context.getInstance().getLocalDB();
|
||||||
|
private final SceneContext sceneContext = Context.getInstance().getSceneContext();
|
||||||
|
|
||||||
|
private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss")
|
||||||
|
.withZone(ZoneId.systemDefault());
|
||||||
|
private static final Map<MessageStatus, Image> statusImages = IconUtil.loadByEnum(MessageStatus.class, 16);
|
||||||
|
private static final Settings settings = Settings.getInstance();
|
||||||
|
private static final Logger logger = EnvoyLog.getLogger(MessageControl.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param message the message that should be formatted
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public MessageControl(Message message) {
|
||||||
|
// Creating the underlying VBox and the dateLabel
|
||||||
|
final var hbox = new HBox();
|
||||||
|
if (message.getSenderID() != localDB.getUser().getID() && message instanceof GroupMessage) {
|
||||||
|
// Displaying the name of the sender in a group
|
||||||
|
final var label = new Label();
|
||||||
|
label.getStyleClass().add("group-member-names");
|
||||||
|
label.setText(localDB.getUsers()
|
||||||
|
.values()
|
||||||
|
.stream()
|
||||||
|
.filter(c -> c.getID() == message.getSenderID())
|
||||||
|
.findFirst()
|
||||||
|
.map(User::getName)
|
||||||
|
.orElse("Unknown User"));
|
||||||
|
label.setPadding(new Insets(0, 5, 0, 0));
|
||||||
|
hbox.getChildren().add(label);
|
||||||
|
}
|
||||||
|
hbox.getChildren().add(new Label(dateFormat.format(message.getCreationDate())));
|
||||||
|
final var vbox = new VBox(hbox);
|
||||||
|
|
||||||
|
// Creating the actions for the MenuItems
|
||||||
|
final var contextMenu = new ContextMenu();
|
||||||
|
final var copyMenuItem = new MenuItem("Copy");
|
||||||
|
final var deleteMenuItem = new MenuItem("Delete");
|
||||||
|
final var forwardMenuItem = new MenuItem("Forward");
|
||||||
|
final var quoteMenuItem = new MenuItem("Quote");
|
||||||
|
final var infoMenuItem = new MenuItem("Info");
|
||||||
|
copyMenuItem.setOnAction(e -> copyMessage(message));
|
||||||
|
deleteMenuItem.setOnAction(e -> deleteMessage(message));
|
||||||
|
forwardMenuItem.setOnAction(e -> forwardMessage(message));
|
||||||
|
quoteMenuItem.setOnAction(e -> quoteMessage(message));
|
||||||
|
infoMenuItem.setOnAction(e -> loadMessageInfoScene(message));
|
||||||
|
contextMenu.getItems().addAll(copyMenuItem, deleteMenuItem, forwardMenuItem, quoteMenuItem, infoMenuItem);
|
||||||
|
|
||||||
|
// Handling message attachment display
|
||||||
|
// TODO: Add missing attachment types
|
||||||
|
if (message.hasAttachment()) {
|
||||||
|
switch (message.getAttachment().getType()) {
|
||||||
|
case PICTURE:
|
||||||
|
vbox.getChildren()
|
||||||
|
.add(new ImageView(new Image(new ByteArrayInputStream(message.getAttachment().getData()), 256, 256, true, true)));
|
||||||
|
break;
|
||||||
|
case VIDEO:
|
||||||
|
break;
|
||||||
|
case VOICE:
|
||||||
|
vbox.getChildren().add(new AudioControl(message.getAttachment().getData()));
|
||||||
|
break;
|
||||||
|
case DOCUMENT:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
final var saveAttachment = new MenuItem("Save attachment");
|
||||||
|
saveAttachment.setOnAction(e -> saveAttachment(message));
|
||||||
|
contextMenu.getItems().add(saveAttachment);
|
||||||
|
}
|
||||||
|
// Creating the textLabel
|
||||||
|
final var textLabel = new Label(message.getText());
|
||||||
|
textLabel.setMaxWidth(430);
|
||||||
|
textLabel.setWrapText(true);
|
||||||
|
final var hBoxBottom = new HBox();
|
||||||
|
hBoxBottom.getChildren().add(textLabel);
|
||||||
|
// Setting the message status icon and background color
|
||||||
|
if (message.getSenderID() == localDB.getUser().getID()) {
|
||||||
|
final var statusIcon = new ImageView(statusImages.get(message.getStatus()));
|
||||||
|
statusIcon.setPreserveRatio(true);
|
||||||
|
final var space = new Region();
|
||||||
|
HBox.setHgrow(space, Priority.ALWAYS);
|
||||||
|
hBoxBottom.getChildren().add(space);
|
||||||
|
hBoxBottom.getChildren().add(statusIcon);
|
||||||
|
hBoxBottom.setAlignment(Pos.BOTTOM_RIGHT);
|
||||||
|
getStyleClass().add("own-message");
|
||||||
|
ownMessage = true;
|
||||||
|
hbox.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
} else {
|
||||||
|
getStyleClass().add("received-message");
|
||||||
|
ownMessage = false;
|
||||||
|
}
|
||||||
|
vbox.getChildren().add(hBoxBottom);
|
||||||
|
// Adjusting height and weight of the cell to the corresponding ListView
|
||||||
|
paddingProperty().setValue(new Insets(5, 20, 5, 20));
|
||||||
|
setContextMenu(contextMenu);
|
||||||
|
setGraphic(vbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context Menu actions
|
||||||
|
|
||||||
|
private void copyMessage(Message message) {
|
||||||
|
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(message.getText()), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteMessage(Message message) { logger.log(Level.FINEST, "message deletion was requested for " + message); }
|
||||||
|
|
||||||
|
private void forwardMessage(Message message) { logger.log(Level.FINEST, "message forwarding was requested for " + message); }
|
||||||
|
|
||||||
|
private void quoteMessage(Message message) { logger.log(Level.FINEST, "message quotation was requested for " + message); }
|
||||||
|
|
||||||
|
private void loadMessageInfoScene(Message message) { logger.log(Level.FINEST, "message info scene was requested for " + message); }
|
||||||
|
|
||||||
|
private void saveAttachment(Message message) {
|
||||||
|
File file;
|
||||||
|
final var fileName = message.getAttachment().getName();
|
||||||
|
final var downloadLocation = settings.getDownloadLocation();
|
||||||
|
// Show save file dialog, if the user did not opt-out
|
||||||
|
if (!settings.isDownloadSavedWithoutAsking()) {
|
||||||
|
final var fileChooser = new FileChooser();
|
||||||
|
fileChooser.setInitialFileName(fileName);
|
||||||
|
fileChooser.setInitialDirectory(downloadLocation);
|
||||||
|
file = fileChooser.showSaveDialog(sceneContext.getStage());
|
||||||
|
} else file = new File(downloadLocation, fileName);
|
||||||
|
|
||||||
|
// A file was selected
|
||||||
|
if (file != null) try (FileOutputStream fos = new FileOutputStream(file)) {
|
||||||
|
fos.write(message.getAttachment().getData());
|
||||||
|
logger.log(Level.FINE, "Attachment of message was saved at " + file.getAbsolutePath());
|
||||||
|
} catch (final IOException e) {
|
||||||
|
logger.log(Level.WARNING, "Could not save attachment of " + message + ": ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return whether the message stored by this {@code MessageControl} has been
|
||||||
|
* sent by this user of Envoy
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public boolean isOwnMessage() { return ownMessage; }
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package envoy.client.ui.control;
|
||||||
|
|
||||||
|
import javafx.scene.image.*;
|
||||||
|
import javafx.scene.shape.Rectangle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a set of convenience constructors for images that are displayed as profile pictures.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public final class ProfilePicImageView extends ImageView {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code ProfilePicImageView} without a default image.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public ProfilePicImageView() { this(null); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code ProfilePicImageView}.
|
||||||
|
*
|
||||||
|
* @param image the image to display
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public ProfilePicImageView(Image image) { this(image, 40); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code ProfilePicImageView}.
|
||||||
|
*
|
||||||
|
* @param image the image to display
|
||||||
|
* @param sizeAndRounding the size and rounding for a circular
|
||||||
|
* {@code ProfilePicImageView}
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public ProfilePicImageView(Image image, double sizeAndRounding) { this(image, sizeAndRounding, sizeAndRounding); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code ProfilePicImageView}.
|
||||||
|
*
|
||||||
|
* @param image the image to display
|
||||||
|
* @param size the size of this {@code ProfilePicImageView}
|
||||||
|
* @param rounding how rounded this {@code ProfilePicImageView} should be
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public ProfilePicImageView(Image image, double size, double rounding) {
|
||||||
|
super(image);
|
||||||
|
final var clip = new Rectangle();
|
||||||
|
clip.setWidth(size);
|
||||||
|
clip.setHeight(size);
|
||||||
|
clip.setArcHeight(rounding);
|
||||||
|
clip.setArcWidth(rounding);
|
||||||
|
setClip(clip);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
package envoy.client.ui.control;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import javafx.event.*;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.input.Clipboard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a context menu that offers an additional option when one of
|
||||||
|
* its menu items has been clicked.
|
||||||
|
* <p>
|
||||||
|
* Current options are:
|
||||||
|
* <ul>
|
||||||
|
* <li>undo</li>
|
||||||
|
* <li>redo</li>
|
||||||
|
* <li>cut</li>
|
||||||
|
* <li>copy</li>
|
||||||
|
* <li>paste</li>
|
||||||
|
* <li>delete</li>
|
||||||
|
* <li>clear</li>
|
||||||
|
* <li>Select all</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
* @apiNote please refrain from using
|
||||||
|
* {@link ContextMenu#setOnShowing(EventHandler)} as this is already
|
||||||
|
* used by this component
|
||||||
|
*/
|
||||||
|
public class TextInputContextMenu extends ContextMenu {
|
||||||
|
|
||||||
|
private final MenuItem undoMI = new MenuItem("Undo");
|
||||||
|
private final MenuItem redoMI = new MenuItem("Redo");
|
||||||
|
private final MenuItem cutMI = new MenuItem("Cut");
|
||||||
|
private final MenuItem copyMI = new MenuItem("Copy");
|
||||||
|
private final MenuItem pasteMI = new MenuItem("Paste");
|
||||||
|
private final MenuItem deleteMI = new MenuItem("Delete selection");
|
||||||
|
private final MenuItem clearMI = new MenuItem("Clear");
|
||||||
|
private final MenuItem selectAllMI = new MenuItem("Select all");
|
||||||
|
private final MenuItem separatorMI = new SeparatorMenuItem();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code TextInputContextMenu} with an optional action when
|
||||||
|
* this menu was clicked. Currently shows:
|
||||||
|
* <ul>
|
||||||
|
* <li>undo</li>
|
||||||
|
* <li>redo</li>
|
||||||
|
* <li>cut</li>
|
||||||
|
* <li>copy</li>
|
||||||
|
* <li>paste</li>
|
||||||
|
* <li>delete</li>
|
||||||
|
* <li>clear</li>
|
||||||
|
* <li>Select all</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param control the text input component to display this
|
||||||
|
* {@code ContextMenu}
|
||||||
|
* @param menuItemClicked the second action to perform when a menu item of this
|
||||||
|
* context menu has been clicked
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
* @apiNote please refrain from using
|
||||||
|
* {@link ContextMenu#setOnShowing(EventHandler)} as this is already
|
||||||
|
* used by this component
|
||||||
|
*/
|
||||||
|
public TextInputContextMenu(TextInputControl control, Consumer<ActionEvent> menuItemClicked) {
|
||||||
|
|
||||||
|
// Define the actions when clicked
|
||||||
|
undoMI.setOnAction(addAction(e -> control.undo(), menuItemClicked));
|
||||||
|
redoMI.setOnAction(addAction(e -> control.redo(), menuItemClicked));
|
||||||
|
cutMI.setOnAction(addAction(e -> control.cut(), menuItemClicked));
|
||||||
|
copyMI.setOnAction(addAction(e -> control.copy(), menuItemClicked));
|
||||||
|
pasteMI.setOnAction(addAction(e -> control.paste(), menuItemClicked));
|
||||||
|
deleteMI.setOnAction(addAction(e -> control.replaceSelection(""), menuItemClicked));
|
||||||
|
clearMI.setOnAction(addAction(e -> control.setText(""), menuItemClicked));
|
||||||
|
selectAllMI.setOnAction(addAction(e -> control.selectAll(), menuItemClicked));
|
||||||
|
|
||||||
|
// Define the times it will be disabled
|
||||||
|
undoMI.disableProperty().bind(control.undoableProperty().not());
|
||||||
|
redoMI.disableProperty().bind(control.redoableProperty().not());
|
||||||
|
cutMI.disableProperty().bind(control.selectedTextProperty().isEmpty());
|
||||||
|
copyMI.disableProperty().bind(control.selectedTextProperty().isEmpty());
|
||||||
|
deleteMI.disableProperty().bind(control.selectedTextProperty().isEmpty());
|
||||||
|
clearMI.disableProperty().bind(control.textProperty().isEmpty());
|
||||||
|
setOnShowing(e -> pasteMI.setDisable(!Clipboard.getSystemClipboard().hasString()));
|
||||||
|
|
||||||
|
selectAllMI.getProperties().put("refreshMenu", Boolean.TRUE);
|
||||||
|
|
||||||
|
// Add all items to the ContextMenu
|
||||||
|
getItems().add(undoMI);
|
||||||
|
getItems().add(redoMI);
|
||||||
|
getItems().add(cutMI);
|
||||||
|
getItems().add(copyMI);
|
||||||
|
getItems().add(pasteMI);
|
||||||
|
getItems().add(separatorMI);
|
||||||
|
getItems().add(deleteMI);
|
||||||
|
getItems().add(clearMI);
|
||||||
|
getItems().add(separatorMI);
|
||||||
|
getItems().add(selectAllMI);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EventHandler<ActionEvent> addAction(Consumer<ActionEvent> originalAction, Consumer<ActionEvent> additionalAction) {
|
||||||
|
return e -> { originalAction.accept(e); additionalAction.accept(e); };
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Defines custom UI controls.
|
||||||
|
*
|
||||||
|
* @author Kai S. K. Engelbart
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @author Maximilian Käfer
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
package envoy.client.ui.control;
|
@ -2,57 +2,57 @@ package envoy.client.ui.controller;
|
|||||||
|
|
||||||
import java.awt.Toolkit;
|
import java.awt.Toolkit;
|
||||||
import java.awt.datatransfer.StringSelection;
|
import java.awt.datatransfer.StringSelection;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.*;
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.*;
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import javafx.animation.RotateTransition;
|
import javafx.animation.RotateTransition;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.fxml.FXML;
|
import javafx.collections.transformation.FilteredList;
|
||||||
import javafx.scene.Node;
|
import javafx.fxml.*;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.Alert.AlertType;
|
import javafx.scene.control.Alert.AlertType;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.*;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.input.*;
|
||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.input.KeyEvent;
|
|
||||||
import javafx.scene.layout.GridPane;
|
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.scene.shape.Rectangle;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
|
|
||||||
import envoy.client.data.*;
|
import envoy.client.data.*;
|
||||||
import envoy.client.data.audio.AudioRecorder;
|
import envoy.client.data.audio.AudioRecorder;
|
||||||
import envoy.client.event.MessageCreationEvent;
|
import envoy.client.data.commands.*;
|
||||||
import envoy.client.net.Client;
|
import envoy.client.event.*;
|
||||||
import envoy.client.net.WriteProxy;
|
import envoy.client.helper.ShutdownHelper;
|
||||||
import envoy.client.ui.IconUtil;
|
import envoy.client.net.*;
|
||||||
import envoy.client.ui.Restorable;
|
import envoy.client.ui.*;
|
||||||
import envoy.client.ui.SceneContext;
|
import envoy.client.ui.SceneContext.SceneInfo;
|
||||||
import envoy.client.ui.listcell.ContactListCellFactory;
|
import envoy.client.ui.control.*;
|
||||||
import envoy.client.ui.listcell.MessageControl;
|
import envoy.client.ui.listcell.*;
|
||||||
import envoy.client.ui.listcell.MessageListCellFactory;
|
import envoy.client.util.*;
|
||||||
import envoy.data.*;
|
import envoy.data.*;
|
||||||
import envoy.data.Attachment.AttachmentType;
|
import envoy.data.Attachment.AttachmentType;
|
||||||
|
import envoy.data.Message.MessageStatus;
|
||||||
import envoy.event.*;
|
import envoy.event.*;
|
||||||
import envoy.event.contact.ContactOperation;
|
import envoy.event.contact.ContactOperation;
|
||||||
import envoy.exception.EnvoyException;
|
import envoy.exception.EnvoyException;
|
||||||
import envoy.util.EnvoyLog;
|
import envoy.util.EnvoyLog;
|
||||||
|
|
||||||
|
import dev.kske.eventbus.*;
|
||||||
|
import dev.kske.eventbus.Event;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project: <strong>envoy-client</strong><br>
|
* Controller for the chat scene.
|
||||||
* File: <strong>ChatSceneController.java</strong><br>
|
|
||||||
* Created: <strong>26.03.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public final class ChatScene implements Restorable {
|
public final class ChatScene implements EventListener, Restorable {
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private GridPane scene;
|
private GridPane scene;
|
||||||
@ -79,7 +79,13 @@ public final class ChatScene implements Restorable {
|
|||||||
private Button settingsButton;
|
private Button settingsButton;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Button rotateButton;
|
private Button messageSearchButton;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button newGroupButton;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button newContactButton;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private TextArea messageTextArea;
|
private TextArea messageTextArea;
|
||||||
@ -96,24 +102,59 @@ public final class ChatScene implements Restorable {
|
|||||||
@FXML
|
@FXML
|
||||||
private ImageView attachmentView;
|
private ImageView attachmentView;
|
||||||
|
|
||||||
private LocalDB localDB;
|
@FXML
|
||||||
private Client client;
|
private Label topBarContactLabel;
|
||||||
private WriteProxy writeProxy;
|
|
||||||
private SceneContext sceneContext;
|
|
||||||
|
|
||||||
private Chat currentChat;
|
@FXML
|
||||||
private AudioRecorder recorder;
|
private Label topBarStatusLabel;
|
||||||
private boolean recording;
|
|
||||||
private Attachment pendingAttachment;
|
|
||||||
private boolean postingPermanentlyDisabled;
|
|
||||||
|
|
||||||
private static final Settings settings = Settings.getInstance();
|
@FXML
|
||||||
private static final EventBus eventBus = EventBus.getInstance();
|
private ImageView clientProfilePic;
|
||||||
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
|
|
||||||
|
|
||||||
private static final Image DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20);
|
@FXML
|
||||||
private static final int MAX_MESSAGE_LENGTH = 255;
|
private ImageView recipientProfilePic;
|
||||||
private static final int DEFAULT_ICON_SIZE = 16;
|
|
||||||
|
@FXML
|
||||||
|
private TextArea contactSearch;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private VBox contactOperations;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TabPane tabPane;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Tab contactSearchTab;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Tab groupCreationTab;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private HBox contactSpecificOnlineOperations;
|
||||||
|
|
||||||
|
private Chat currentChat;
|
||||||
|
private FilteredList<Chat> chats;
|
||||||
|
private boolean recording;
|
||||||
|
private Attachment pendingAttachment;
|
||||||
|
private boolean postingPermanentlyDisabled;
|
||||||
|
private boolean isCustomAttachmentImage;
|
||||||
|
|
||||||
|
private final LocalDB localDB = context.getLocalDB();
|
||||||
|
private final Client client = context.getClient();
|
||||||
|
private final WriteProxy writeProxy = context.getWriteProxy();
|
||||||
|
private final SceneContext sceneContext = context.getSceneContext();
|
||||||
|
private final AudioRecorder recorder = new AudioRecorder();
|
||||||
|
private final SystemCommandMap messageTextAreaCommands = new SystemCommandMap();
|
||||||
|
private final Tooltip onlyIfOnlineTooltip = new Tooltip("You need to be online to do this");
|
||||||
|
|
||||||
|
private static Image DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20);
|
||||||
|
|
||||||
|
private static final Settings settings = Settings.getInstance();
|
||||||
|
private static final EventBus eventBus = EventBus.getInstance();
|
||||||
|
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
|
||||||
|
private static final Context context = Context.getInstance();
|
||||||
|
private static final int MAX_MESSAGE_LENGTH = 255;
|
||||||
|
private static final int DEFAULT_ICON_SIZE = 16;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the appearance of certain visual components.
|
* Initializes the appearance of certain visual components.
|
||||||
@ -122,105 +163,179 @@ public final class ChatScene implements Restorable {
|
|||||||
*/
|
*/
|
||||||
@FXML
|
@FXML
|
||||||
private void initialize() {
|
private void initialize() {
|
||||||
|
eventBus.registerListener(this);
|
||||||
|
|
||||||
// Initialize message and user rendering
|
// Initialize message and user rendering
|
||||||
messageList.setCellFactory(MessageListCellFactory::new);
|
messageList.setCellFactory(MessageListCell::new);
|
||||||
chatList.setCellFactory(ContactListCellFactory::new);
|
chatList.setCellFactory(new ListCellFactory<>(ChatControl::new));
|
||||||
|
|
||||||
|
// JavaFX provides an internal way of populating the context menu of a text
|
||||||
|
// area.
|
||||||
|
// We, however, need additional functionality.
|
||||||
|
messageTextArea.setContextMenu(new TextInputContextMenu(messageTextArea, e -> checkKeyCombination(null)));
|
||||||
|
|
||||||
|
// Set the icons of buttons and image views
|
||||||
settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
|
settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
|
||||||
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
|
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
|
||||||
attachmentButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE)));
|
attachmentButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE)));
|
||||||
attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
|
attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
|
||||||
rotateButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("rotate", (int) (DEFAULT_ICON_SIZE * 1.5))));
|
messageSearchButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("search", DEFAULT_ICON_SIZE)));
|
||||||
|
clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
|
||||||
|
onlyIfOnlineTooltip.setShowDelay(Duration.millis(250));
|
||||||
|
final var clip = new Rectangle();
|
||||||
|
clip.setWidth(43);
|
||||||
|
clip.setHeight(43);
|
||||||
|
clip.setArcHeight(43);
|
||||||
|
clip.setArcWidth(43);
|
||||||
|
clientProfilePic.setClip(clip);
|
||||||
|
|
||||||
// Listen to received messages
|
chatList.setItems(chats = new FilteredList<>(localDB.getChats()));
|
||||||
eventBus.register(MessageCreationEvent.class, e -> {
|
contactLabel.setText(localDB.getUser().getName());
|
||||||
final var message = e.get();
|
|
||||||
localDB.getChat(message instanceof GroupMessage ? message.getRecipientID() : message.getSenderID()).ifPresent(chat -> {
|
|
||||||
chat.insert(message);
|
|
||||||
if (chat.equals(currentChat)) {
|
|
||||||
try {
|
|
||||||
currentChat.read(writeProxy);
|
|
||||||
} catch (final IOException e1) {
|
|
||||||
logger.log(Level.WARNING, "Could not read current chat: ", e1);
|
|
||||||
}
|
|
||||||
Platform.runLater(() -> { messageList.refresh(); scrollToMessageListEnd(); });
|
|
||||||
} else chat.incrementUnreadAmount();
|
|
||||||
// Moving chat with most recent unreadMessages to the top
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
chatList.getItems().remove(chat);
|
|
||||||
chatList.getItems().add(0, chat);
|
|
||||||
if (chat.equals(currentChat)) chatList.getSelectionModel().select(0);
|
|
||||||
localDB.getChats().remove(chat);
|
|
||||||
localDB.getChats().add(0, chat);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen to message status changes
|
initializeSystemCommandsMap();
|
||||||
eventBus.register(MessageStatusChange.class, e -> localDB.getMessage(e.getID()).ifPresent(message -> {
|
|
||||||
message.setStatus(e.get());
|
|
||||||
// Update UI if in current chat
|
|
||||||
if (currentChat != null && message.getSenderID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh);
|
|
||||||
}));
|
|
||||||
|
|
||||||
eventBus.register(GroupMessageStatusChange.class, e -> localDB.getMessage(e.getID()).ifPresent(groupMessage -> {
|
Platform.runLater(() -> {
|
||||||
((GroupMessage) groupMessage).getMemberStatuses().replace(e.getMemberID(), e.get());
|
final var online = client.isOnline();
|
||||||
|
// no check will be performed in case it has already been disabled - a negative
|
||||||
// Update UI if in current chat
|
// GroupCreationResult might have been returned
|
||||||
if (currentChat != null && groupMessage.getRecipientID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh);
|
if (!newGroupButton.isDisabled()) newGroupButton.setDisable(!online);
|
||||||
}));
|
newContactButton.setDisable(!online);
|
||||||
|
if (online) try {
|
||||||
// Listen to user status changes
|
Tooltip.uninstall(contactSpecificOnlineOperations, onlyIfOnlineTooltip);
|
||||||
eventBus.register(UserStatusChange.class,
|
contactSearchTab.setContent(new FXMLLoader().load(getClass().getResourceAsStream("/fxml/ContactSearchTab.fxml")));
|
||||||
e -> chatList.getItems()
|
groupCreationTab.setContent(new FXMLLoader().load(getClass().getResourceAsStream("/fxml/GroupCreationTab.fxml")));
|
||||||
.stream()
|
} catch (final IOException e) {
|
||||||
.filter(c -> c.getRecipient().getID() == e.getID())
|
logger.log(Level.SEVERE, "An error occurred when attempting to load tabs: ", e);
|
||||||
.findAny()
|
}
|
||||||
.map(Chat::getRecipient)
|
else {
|
||||||
.ifPresent(u -> { ((User) u).setStatus(e.get()); Platform.runLater(chatList::refresh); }));
|
Tooltip.install(contactSpecificOnlineOperations, onlyIfOnlineTooltip);
|
||||||
|
updateInfoLabel("You are offline", "info-label-warning");
|
||||||
// Listen to contacts changes
|
|
||||||
eventBus.register(ContactOperation.class, e -> {
|
|
||||||
final var contact = e.get();
|
|
||||||
switch (e.getOperationType()) {
|
|
||||||
case ADD:
|
|
||||||
localDB.getUsers().put(contact.getName(), contact);
|
|
||||||
Chat chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact);
|
|
||||||
localDB.getChats().add(chat);
|
|
||||||
Platform.runLater(() -> chatList.getItems().add(chat));
|
|
||||||
break;
|
|
||||||
case REMOVE:
|
|
||||||
localDB.getUsers().remove(contact.getName());
|
|
||||||
localDB.getChats().removeIf(c -> c.getRecipient().getID() == contact.getID());
|
|
||||||
Platform.runLater(() -> chatList.getItems().removeIf(c -> c.getRecipient().getID() == contact.getID()));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Event(eventType = BackEvent.class)
|
||||||
|
private void onBackEvent() { tabPane.getSelectionModel().select(Tabs.CONTACT_LIST.ordinal()); }
|
||||||
|
|
||||||
|
@Event(includeSubtypes = true)
|
||||||
|
private void onMessage(Message message) {
|
||||||
|
|
||||||
|
// The sender of the message is the recipient of the chat
|
||||||
|
// Exceptions: this user is the sender (sync) or group message (group is
|
||||||
|
// recipient)
|
||||||
|
final boolean ownMessage = message.getSenderID() == localDB.getUser().getID();
|
||||||
|
final var recipientID = message instanceof GroupMessage || ownMessage ? message.getRecipientID() : message.getSenderID();
|
||||||
|
|
||||||
|
localDB.getChat(recipientID).ifPresent(chat -> {
|
||||||
|
chat.insert(message);
|
||||||
|
|
||||||
|
// Read current chat or increment unread amount
|
||||||
|
if (chat.equals(currentChat)) {
|
||||||
|
currentChat.read(writeProxy);
|
||||||
|
Platform.runLater(this::scrollToMessageListEnd);
|
||||||
|
} else if (!ownMessage && message.getStatus() != MessageStatus.READ) chat.incrementUnreadAmount();
|
||||||
|
|
||||||
|
// Move chat with most recent unread messages to the top
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
chats.getSource().remove(chat);
|
||||||
|
((ObservableList<Chat>) chats.getSource()).add(0, chat);
|
||||||
|
|
||||||
|
if (chat.equals(currentChat)) chatList.getSelectionModel().select(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Event
|
||||||
|
private void onMessageStatusChange(MessageStatusChange evt) {
|
||||||
|
|
||||||
|
// Update UI if in current chat and the current user was the sender of the
|
||||||
|
// message
|
||||||
|
if (currentChat != null) localDB.getMessage(evt.getID())
|
||||||
|
.filter(msg -> msg.getSenderID() == client.getSender().getID())
|
||||||
|
.ifPresent(msg -> Platform.runLater(messageList::refresh));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Event(eventType = UserStatusChange.class)
|
||||||
|
private void onUserStatusChange() { Platform.runLater(chatList::refresh); }
|
||||||
|
|
||||||
|
@Event
|
||||||
|
private void onContactOperation(ContactOperation operation) {
|
||||||
|
final var contact = operation.get();
|
||||||
|
switch (operation.getOperationType()) {
|
||||||
|
case ADD:
|
||||||
|
if (contact instanceof User) localDB.getUsers().put(contact.getName(), (User) contact);
|
||||||
|
final var chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact);
|
||||||
|
Platform.runLater(() -> ((ObservableList<Chat>) chats.getSource()).add(0, chat));
|
||||||
|
break;
|
||||||
|
case REMOVE:
|
||||||
|
Platform.runLater(() -> chats.getSource().removeIf(c -> c.getRecipient().equals(contact)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Event(eventType = NoAttachments.class)
|
||||||
|
private void onNoAttachments() {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
attachmentButton.setDisable(true);
|
||||||
|
voiceButton.setDisable(true);
|
||||||
|
final var alert = new Alert(AlertType.ERROR);
|
||||||
|
alert.setTitle("No attachments possible");
|
||||||
|
alert.setHeaderText("Your current server does not support attachments.");
|
||||||
|
alert.setContentText("If this is unplanned, please contact your server administrator.");
|
||||||
|
alert.showAndWait();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Event
|
||||||
|
private void onGroupCreationResult(GroupCreationResult result) { Platform.runLater(() -> newGroupButton.setDisable(!result.get())); }
|
||||||
|
|
||||||
|
@Event(eventType = ThemeChangeEvent.class)
|
||||||
|
private void onThemeChange() {
|
||||||
|
settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
|
||||||
|
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
|
||||||
|
attachmentButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE)));
|
||||||
|
DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20);
|
||||||
|
attachmentView.setImage(isCustomAttachmentImage ? attachmentView.getImage() : DEFAULT_ATTACHMENT_VIEW_IMAGE);
|
||||||
|
messageSearchButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("search", DEFAULT_ICON_SIZE)));
|
||||||
|
clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
|
||||||
|
chatList.setCellFactory(new ListCellFactory<>(ChatControl::new));
|
||||||
|
messageList.setCellFactory(MessageListCell::new);
|
||||||
|
// TODO: cache image
|
||||||
|
if (currentChat.getRecipient() instanceof User) recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
|
||||||
|
else recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Event(eventType = Logout.class, priority = 200)
|
||||||
|
private void onLogout() { eventBus.removeListener(this); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes all necessary data via dependency injection-
|
* Initializes all {@code SystemCommands} used in {@code ChatScene}.
|
||||||
*
|
*
|
||||||
* @param sceneContext the scene context used to load other scenes
|
* @since Envoy Client v0.2-beta
|
||||||
* @param localDB the local database form which chats and users are loaded
|
|
||||||
* @param client the client used to request ID generators
|
|
||||||
* @param writeProxy the write proxy used to send messages and other data to
|
|
||||||
* the server
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
*/
|
||||||
public void initializeData(SceneContext sceneContext, LocalDB localDB, Client client, WriteProxy writeProxy) {
|
private void initializeSystemCommandsMap() {
|
||||||
this.sceneContext = sceneContext;
|
final var builder = new SystemCommandBuilder(messageTextAreaCommands);
|
||||||
this.localDB = localDB;
|
|
||||||
this.client = client;
|
|
||||||
this.writeProxy = writeProxy;
|
|
||||||
|
|
||||||
chatList.setItems(FXCollections.observableList(localDB.getChats()));
|
// Do A Barrel roll initialization
|
||||||
contactLabel.setText(localDB.getUser().getName());
|
final var random = new Random();
|
||||||
MessageControl.setUser(localDB.getUser());
|
builder.setAction(text -> doABarrelRoll(Integer.parseInt(text.get(0)), Double.parseDouble(text.get(1))))
|
||||||
if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info");
|
.setDefaults(Integer.toString(random.nextInt(3) + 1), Double.toString(random.nextDouble() * 3 + 1))
|
||||||
|
.setDescription("See for yourself :)")
|
||||||
|
.setNumberOfArguments(2)
|
||||||
|
.build("dabr");
|
||||||
|
|
||||||
recorder = new AudioRecorder();
|
// Logout initialization
|
||||||
|
builder.setAction(text -> ShutdownHelper.logout()).setNumberOfArguments(0).setDescription("Logs you out.").build("logout");
|
||||||
|
|
||||||
|
// Exit initialization
|
||||||
|
builder.setAction(text -> ShutdownHelper.exit()).setNumberOfArguments(0).setDescription("Exits the program").build("exit", false);
|
||||||
|
builder.build("q");
|
||||||
|
|
||||||
|
// Open settings scene initialization
|
||||||
|
builder.setAction(text -> sceneContext.load(SceneInfo.SETTINGS_SCENE))
|
||||||
|
.setNumberOfArguments(0)
|
||||||
|
.setDescription("Opens the settings screen")
|
||||||
|
.build("settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -233,7 +348,9 @@ public final class ChatScene implements Restorable {
|
|||||||
*/
|
*/
|
||||||
@FXML
|
@FXML
|
||||||
private void chatListClicked() {
|
private void chatListClicked() {
|
||||||
final Contact user = chatList.getSelectionModel().getSelectedItem().getRecipient();
|
if (chatList.getSelectionModel().isEmpty()) return;
|
||||||
|
|
||||||
|
final var user = chatList.getSelectionModel().getSelectedItem().getRecipient();
|
||||||
if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) {
|
if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) {
|
||||||
|
|
||||||
// LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes
|
// LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes
|
||||||
@ -241,18 +358,14 @@ public final class ChatScene implements Restorable {
|
|||||||
// Load the chat
|
// Load the chat
|
||||||
currentChat = localDB.getChat(user.getID()).get();
|
currentChat = localDB.getChat(user.getID()).get();
|
||||||
|
|
||||||
messageList.setItems(FXCollections.observableList(currentChat.getMessages()));
|
messageList.setItems(currentChat.getMessages());
|
||||||
final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount() - 1;
|
final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount();
|
||||||
messageList.scrollTo(scrollIndex);
|
messageList.scrollTo(scrollIndex);
|
||||||
logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex);
|
logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex);
|
||||||
deleteContactMenuItem.setText("Delete " + user.getName());
|
deleteContactMenuItem.setText("Delete " + user.getName());
|
||||||
|
|
||||||
// Read the current chat
|
// Read the current chat
|
||||||
try {
|
currentChat.read(writeProxy);
|
||||||
currentChat.read(writeProxy);
|
|
||||||
} catch (final IOException e) {
|
|
||||||
logger.log(Level.WARNING, "Could not read current chat.", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discard the pending attachment
|
// Discard the pending attachment
|
||||||
if (recorder.isRecording()) {
|
if (recorder.isRecording()) {
|
||||||
@ -270,6 +383,27 @@ public final class ChatScene implements Restorable {
|
|||||||
voiceButton.setDisable(!recorder.isSupported());
|
voiceButton.setDisable(!recorder.isSupported());
|
||||||
attachmentButton.setDisable(false);
|
attachmentButton.setDisable(false);
|
||||||
chatList.refresh();
|
chatList.refresh();
|
||||||
|
|
||||||
|
if (currentChat != null) {
|
||||||
|
topBarContactLabel.setText(currentChat.getRecipient().getName());
|
||||||
|
if (currentChat.getRecipient() instanceof User) {
|
||||||
|
final String status = ((User) currentChat.getRecipient()).getStatus().toString();
|
||||||
|
topBarStatusLabel.setText(status);
|
||||||
|
topBarStatusLabel.getStyleClass().add(status.toLowerCase());
|
||||||
|
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
|
||||||
|
} else {
|
||||||
|
topBarStatusLabel.setText(currentChat.getRecipient().getContacts().size() + " members");
|
||||||
|
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
|
||||||
|
}
|
||||||
|
final Rectangle clip = new Rectangle();
|
||||||
|
clip.setWidth(43);
|
||||||
|
clip.setHeight(43);
|
||||||
|
clip.setArcHeight(43);
|
||||||
|
clip.setArcWidth(43);
|
||||||
|
recipientProfilePic.setClip(clip);
|
||||||
|
|
||||||
|
messageSearchButton.setVisible(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -278,10 +412,7 @@ public final class ChatScene implements Restorable {
|
|||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
@FXML
|
@FXML
|
||||||
private void settingsButtonClicked() {
|
private void settingsButtonClicked() { sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE); }
|
||||||
sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE);
|
|
||||||
sceneContext.<SettingsScene>getController().initializeData(sceneContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Actions to perform when the "Add Contact" - Button has been clicked.
|
* Actions to perform when the "Add Contact" - Button has been clicked.
|
||||||
@ -289,10 +420,10 @@ public final class ChatScene implements Restorable {
|
|||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
@FXML
|
@FXML
|
||||||
private void addContactButtonClicked() {
|
private void addContactButtonClicked() { tabPane.getSelectionModel().select(Tabs.CONTACT_SEARCH.ordinal()); }
|
||||||
sceneContext.load(SceneContext.SceneInfo.CONTACT_SEARCH_SCENE);
|
|
||||||
sceneContext.<ContactSearchScene>getController().initializeData(sceneContext, localDB);
|
@FXML
|
||||||
}
|
private void groupCreationButtonClicked() { tabPane.getSelectionModel().select(Tabs.GROUP_CREATION.ordinal()); }
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private void voiceButtonClicked() {
|
private void voiceButtonClicked() {
|
||||||
@ -306,7 +437,9 @@ public final class ChatScene implements Restorable {
|
|||||||
});
|
});
|
||||||
recorder.start();
|
recorder.start();
|
||||||
} else {
|
} else {
|
||||||
pendingAttachment = new Attachment(recorder.finish(), AttachmentType.VOICE);
|
pendingAttachment = new Attachment(recorder.finish(), "Voice_recording_"
|
||||||
|
+ DateTimeFormatter.ofPattern("yyyy_MM_dd-HH_mm_ss").format(LocalDateTime.now()) + "." + AudioRecorder.FILE_FORMAT,
|
||||||
|
AttachmentType.VOICE);
|
||||||
recording = false;
|
recording = false;
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
|
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
|
||||||
@ -344,7 +477,7 @@ public final class ChatScene implements Restorable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get attachment type (default is document)
|
// Get attachment type (default is document)
|
||||||
AttachmentType type = AttachmentType.DOCUMENT;
|
var type = AttachmentType.DOCUMENT;
|
||||||
switch (fileChooser.getSelectedExtensionFilter().getDescription()) {
|
switch (fileChooser.getSelectedExtensionFilter().getDescription()) {
|
||||||
case "Pictures":
|
case "Pictures":
|
||||||
type = AttachmentType.PICTURE;
|
type = AttachmentType.PICTURE;
|
||||||
@ -357,10 +490,13 @@ public final class ChatScene implements Restorable {
|
|||||||
// Create the pending attachment
|
// Create the pending attachment
|
||||||
try {
|
try {
|
||||||
final var fileBytes = Files.readAllBytes(file.toPath());
|
final var fileBytes = Files.readAllBytes(file.toPath());
|
||||||
pendingAttachment = new Attachment(fileBytes, type);
|
pendingAttachment = new Attachment(fileBytes, file.getName(), type);
|
||||||
|
checkPostConditions(false);
|
||||||
// Setting the preview image as image of the attachmentView
|
// Setting the preview image as image of the attachmentView
|
||||||
if (type == AttachmentType.PICTURE)
|
if (type == AttachmentType.PICTURE) {
|
||||||
attachmentView.setImage(new Image(new ByteArrayInputStream(fileBytes), DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, true, true));
|
attachmentView.setImage(new Image(new ByteArrayInputStream(fileBytes), DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, true, true));
|
||||||
|
isCustomAttachmentImage = true;
|
||||||
|
}
|
||||||
attachmentView.setVisible(true);
|
attachmentView.setVisible(true);
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
new Alert(AlertType.ERROR, "The selected file could not be loaded!").showAndWait();
|
new Alert(AlertType.ERROR, "The selected file could not be loaded!").showAndWait();
|
||||||
@ -369,20 +505,27 @@ public final class ChatScene implements Restorable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rotates every element in our application by 360° in at most 2.75s.
|
* Rotates every element in our application by {@code rotations}*360° in
|
||||||
|
* {@code an}.
|
||||||
*
|
*
|
||||||
|
* @param rotations the amount of times the scene is rotated by 360°
|
||||||
|
* @param animationTime the time in seconds that this animation lasts
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
@FXML
|
private void doABarrelRoll(int rotations, double animationTime) {
|
||||||
private void doABarrelRoll() {
|
// Limiting the rotations and duration
|
||||||
// contains all Node objects in ChatScene in alphabetical order
|
rotations = Math.min(rotations, 100000);
|
||||||
final var rotatableNodes = new Node[] { attachmentButton, attachmentView, contactLabel, infoLabel, messageList, messageTextArea,
|
rotations = Math.max(rotations, 1);
|
||||||
postButton, remainingChars, rotateButton, scene, settingsButton, chatList, voiceButton };
|
animationTime = Math.min(animationTime, 150);
|
||||||
final var random = new Random();
|
animationTime = Math.max(animationTime, 0.25);
|
||||||
|
|
||||||
|
// contains all Node objects in ChatScene
|
||||||
|
final var rotatableNodes = ReflectionUtil.getAllDeclaredNodeVariables(this);
|
||||||
for (final var node : rotatableNodes) {
|
for (final var node : rotatableNodes) {
|
||||||
// Defines at most four whole rotation in at most 4s
|
// Sets the animation duration to {animationTime}
|
||||||
final var rotateTransition = new RotateTransition(Duration.seconds(random.nextDouble() * 3 + 1), node);
|
final var rotateTransition = new RotateTransition(Duration.seconds(animationTime), node);
|
||||||
rotateTransition.setByAngle((random.nextInt(3) + 1) * 360);
|
// rotates every element {rotations} times
|
||||||
|
rotateTransition.setByAngle(rotations * 360);
|
||||||
rotateTransition.play();
|
rotateTransition.play();
|
||||||
// This is needed as for some strange reason objects could stop before being
|
// This is needed as for some strange reason objects could stop before being
|
||||||
// rotated back to 0°
|
// rotated back to 0°
|
||||||
@ -400,10 +543,35 @@ public final class ChatScene implements Restorable {
|
|||||||
*/
|
*/
|
||||||
@FXML
|
@FXML
|
||||||
private void checkKeyCombination(KeyEvent e) {
|
private void checkKeyCombination(KeyEvent e) {
|
||||||
|
|
||||||
// Checks whether the text is too long
|
// Checks whether the text is too long
|
||||||
messageTextUpdated();
|
messageTextUpdated();
|
||||||
// Automatic sending of messages via (ctrl +) enter
|
|
||||||
checkPostConditions(e);
|
// Sending an IsTyping event if none has been sent for
|
||||||
|
// IsTyping#millisecondsActive
|
||||||
|
if (client.isOnline() && currentChat.getLastWritingEvent() + IsTyping.millisecondsActive <= System.currentTimeMillis()) {
|
||||||
|
client.send(new IsTyping(getChatID(), currentChat.getRecipient().getID()));
|
||||||
|
currentChat.lastWritingEventWasNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyPressed will be called before the char has been added to the text, hence
|
||||||
|
// this is needed for the first char
|
||||||
|
if (messageTextArea.getText().length() == 1 && e != null) checkPostConditions(e);
|
||||||
|
|
||||||
|
// This is needed for the messageTA context menu
|
||||||
|
else if (e == null) checkPostConditions(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the id that should be used to send things to the server: the id of
|
||||||
|
* 'our' {@link User} if the recipient of that object is another User, else the
|
||||||
|
* id of the {@link Group} 'our' user is sending to.
|
||||||
|
*
|
||||||
|
* @return an id that can be sent to the server
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
private long getChatID() {
|
||||||
|
return currentChat.getRecipient() instanceof User ? client.getSender().getID() : currentChat.getRecipient().getID();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -412,20 +580,37 @@ public final class ChatScene implements Restorable {
|
|||||||
*/
|
*/
|
||||||
@FXML
|
@FXML
|
||||||
private void checkPostConditions(KeyEvent e) {
|
private void checkPostConditions(KeyEvent e) {
|
||||||
checkPostConditions(settings.isEnterToSend() && e.getCode() == KeyCode.ENTER
|
final var enterPressed = e.getCode() == KeyCode.ENTER;
|
||||||
|| !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown());
|
final var messagePosted = enterPressed ? settings.isEnterToSend() ? !e.isControlDown() : e.isControlDown() : false;
|
||||||
|
if (messagePosted) {
|
||||||
|
|
||||||
|
// Removing an inserted line break if added by pressing enter
|
||||||
|
final var text = messageTextArea.getText();
|
||||||
|
final var textPosition = messageTextArea.getCaretPosition() - 1;
|
||||||
|
if (!e.isControlDown() && !text.isEmpty() && text.charAt(textPosition) == '\n')
|
||||||
|
messageTextArea.setText(new StringBuilder(text).deleteCharAt(textPosition).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// if control is pressed, the enter press is originally invalidated. Here it'll
|
||||||
|
// be inserted again
|
||||||
|
else if (enterPressed && e.isControlDown()) {
|
||||||
|
var caretPosition = messageTextArea.getCaretPosition();
|
||||||
|
messageTextArea.setText(new StringBuilder(messageTextArea.getText()).insert(caretPosition, '\n').toString());
|
||||||
|
messageTextArea.positionCaret(++caretPosition);
|
||||||
|
}
|
||||||
|
checkPostConditions(messagePosted);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkPostConditions(boolean sendKeyPressed) {
|
private void checkPostConditions(boolean postMessage) {
|
||||||
if (!postingPermanentlyDisabled) {
|
if (!postingPermanentlyDisabled) {
|
||||||
if (!postButton.isDisabled() && sendKeyPressed) postMessage();
|
if (!postButton.isDisabled() && postMessage) postMessage();
|
||||||
postButton.setDisable(messageTextArea.getText().isBlank() && pendingAttachment == null || currentChat == null);
|
postButton.setDisable(messageTextArea.getText().isBlank() && pendingAttachment == null || currentChat == null);
|
||||||
} else {
|
} else {
|
||||||
final var noMoreMessaging = "Go online to send messages";
|
final var noMoreMessaging = "Go online to send messages";
|
||||||
if (!infoLabel.getText().equals(noMoreMessaging))
|
if (!infoLabel.getText().equals(noMoreMessaging))
|
||||||
// Informing the user that he is a f*cking moron and should use Envoy online
|
// Informing the user that he is a f*cking moron and should use Envoy online
|
||||||
// because he ran out of messageIDs to use
|
// because he ran out of messageIDs to use
|
||||||
updateInfoLabel(noMoreMessaging, "infoLabel-error");
|
updateInfoLabel(noMoreMessaging, "info-label-error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,8 +636,8 @@ public final class ChatScene implements Restorable {
|
|||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
private void updateRemainingCharsLabel() {
|
private void updateRemainingCharsLabel() {
|
||||||
final int currentLength = messageTextArea.getText().length();
|
final var currentLength = messageTextArea.getText().length();
|
||||||
final int remainingLength = MAX_MESSAGE_LENGTH - currentLength;
|
final var remainingLength = MAX_MESSAGE_LENGTH - currentLength;
|
||||||
remainingChars.setText(String.format("remaining chars: %d/%d", remainingLength, MAX_MESSAGE_LENGTH));
|
remainingChars.setText(String.format("remaining chars: %d/%d", remainingLength, MAX_MESSAGE_LENGTH));
|
||||||
remainingChars.setTextFill(Color.rgb(currentLength, remainingLength, 0, 1));
|
remainingChars.setTextFill(Color.rgb(currentLength, remainingLength, 0, 1));
|
||||||
}
|
}
|
||||||
@ -470,11 +655,11 @@ public final class ChatScene implements Restorable {
|
|||||||
postButton.setDisable(true);
|
postButton.setDisable(true);
|
||||||
messageTextArea.setDisable(true);
|
messageTextArea.setDisable(true);
|
||||||
messageTextArea.clear();
|
messageTextArea.clear();
|
||||||
updateInfoLabel("You need to go online to send more messages", "infoLabel-error");
|
updateInfoLabel("You need to go online to send more messages", "info-label-error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final var text = messageTextArea.getText().strip();
|
final var text = messageTextArea.getText().strip();
|
||||||
try {
|
if (!messageTextAreaCommands.executeIfAnyPresent(text)) {
|
||||||
// Creating the message and its metadata
|
// Creating the message and its metadata
|
||||||
final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
|
final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
|
||||||
.setText(text);
|
.setText(text);
|
||||||
@ -495,27 +680,23 @@ public final class ChatScene implements Restorable {
|
|||||||
currentChat.insert(message);
|
currentChat.insert(message);
|
||||||
// Moving currentChat to the top
|
// Moving currentChat to the top
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
chatList.getItems().remove(currentChat);
|
chats.getSource().remove(currentChat);
|
||||||
chatList.getItems().add(0, currentChat);
|
((ObservableList<Chat>) chats.getSource()).add(0, currentChat);
|
||||||
chatList.getSelectionModel().select(0);
|
chatList.getSelectionModel().select(0);
|
||||||
localDB.getChats().remove(currentChat);
|
localDB.getChats().remove(currentChat);
|
||||||
localDB.getChats().add(0, currentChat);
|
localDB.getChats().add(0, currentChat);
|
||||||
});
|
});
|
||||||
messageList.refresh();
|
|
||||||
scrollToMessageListEnd();
|
scrollToMessageListEnd();
|
||||||
|
|
||||||
// Request a new ID generator if all IDs were used
|
// Request a new ID generator if all IDs were used
|
||||||
if (!localDB.getIDGenerator().hasNext() && client.isOnline()) client.requestIdGenerator();
|
if (!localDB.getIDGenerator().hasNext() && client.isOnline()) client.requestIDGenerator();
|
||||||
|
|
||||||
} catch (final IOException e) {
|
|
||||||
logger.log(Level.SEVERE, "Error while sending message: ", e);
|
|
||||||
new Alert(AlertType.ERROR, "An error occured while sending the message!").showAndWait();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear text field and disable post button
|
// Clear text field and disable post button
|
||||||
messageTextArea.setText("");
|
messageTextArea.setText("");
|
||||||
postButton.setDisable(true);
|
postButton.setDisable(true);
|
||||||
updateRemainingCharsLabel();
|
updateRemainingCharsLabel();
|
||||||
|
isCustomAttachmentImage = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -541,9 +722,8 @@ public final class ChatScene implements Restorable {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the {@code attachmentView} in terms of visibility.<br>
|
* Updates the {@code attachmentView} in terms of visibility.<br>
|
||||||
* Additionally resets the shown image to
|
* Additionally resets the shown image to {@code DEFAULT_ATTACHMENT_VIEW_IMAGE}
|
||||||
* {@code DEFAULT_ATTACHMENT_VIEW_IMAGE} if another image is currently
|
* if another image is currently present.
|
||||||
* present.
|
|
||||||
*
|
*
|
||||||
* @param visible whether the {@code attachmentView} should be displayed
|
* @param visible whether the {@code attachmentView} should be displayed
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
@ -562,9 +742,20 @@ public final class ChatScene implements Restorable {
|
|||||||
private void copyAndPostMessage() {
|
private void copyAndPostMessage() {
|
||||||
final var messageText = messageTextArea.getText();
|
final var messageText = messageTextArea.getText();
|
||||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(messageText), null);
|
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(messageText), null);
|
||||||
|
final var image = attachmentView.getImage();
|
||||||
|
final var messageAttachment = pendingAttachment;
|
||||||
postMessage();
|
postMessage();
|
||||||
messageTextArea.setText(messageText);
|
messageTextArea.setText(messageText);
|
||||||
updateRemainingCharsLabel();
|
updateRemainingCharsLabel();
|
||||||
postButton.setDisable(messageText.isBlank());
|
postButton.setDisable(messageText.isBlank());
|
||||||
|
attachmentView.setImage(image);
|
||||||
|
if (attachmentView.getImage() != null) attachmentView.setVisible(true);
|
||||||
|
pendingAttachment = messageAttachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void searchContacts() {
|
||||||
|
chats.setPredicate(contactSearch.getText().isBlank() ? c -> true
|
||||||
|
: c -> c.getRecipient().getName().toLowerCase().contains(contactSearch.getText().toLowerCase()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,130 +0,0 @@
|
|||||||
package envoy.client.ui.controller;
|
|
||||||
|
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.fxml.FXML;
|
|
||||||
import javafx.scene.control.Alert;
|
|
||||||
import javafx.scene.control.Alert.AlertType;
|
|
||||||
import javafx.scene.control.ButtonType;
|
|
||||||
import javafx.scene.control.ListView;
|
|
||||||
|
|
||||||
import envoy.client.data.Chat;
|
|
||||||
import envoy.client.data.LocalDB;
|
|
||||||
import envoy.client.event.SendEvent;
|
|
||||||
import envoy.client.ui.ClearableTextField;
|
|
||||||
import envoy.client.ui.SceneContext;
|
|
||||||
import envoy.client.ui.listcell.ContactListCellFactory;
|
|
||||||
import envoy.event.ElementOperation;
|
|
||||||
import envoy.event.EventBus;
|
|
||||||
import envoy.event.contact.ContactOperation;
|
|
||||||
import envoy.event.contact.ContactSearchRequest;
|
|
||||||
import envoy.event.contact.ContactSearchResult;
|
|
||||||
import envoy.util.EnvoyLog;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>ContactSearchSceneController.java</strong><br>
|
|
||||||
* Created: <strong>07.06.2020</strong><br>
|
|
||||||
*
|
|
||||||
* @author Leon Hofmeister
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public class ContactSearchScene {
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ClearableTextField searchBar;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ListView<Chat> chatList;
|
|
||||||
|
|
||||||
private SceneContext sceneContext;
|
|
||||||
|
|
||||||
private LocalDB localDB;
|
|
||||||
|
|
||||||
private static EventBus eventBus = EventBus.getInstance();
|
|
||||||
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param sceneContext enables the user to return to the chat scene
|
|
||||||
* @param localDB the local database to which new contacts are added
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public void initializeData(SceneContext sceneContext, LocalDB localDB) {
|
|
||||||
this.sceneContext = sceneContext;
|
|
||||||
this.localDB = localDB;
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void initialize() {
|
|
||||||
chatList.setCellFactory(ContactListCellFactory::new);
|
|
||||||
searchBar.setClearButtonListener(e -> { searchBar.getTextField().clear(); chatList.getItems().clear(); });
|
|
||||||
eventBus.register(ContactSearchResult.class,
|
|
||||||
response -> Platform.runLater(() -> {
|
|
||||||
chatList.getItems().clear();
|
|
||||||
chatList.getItems().addAll(response.get().stream().map(Chat::new).collect(Collectors.toList()));
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disables the clear and search button if no text is present in the search bar.
|
|
||||||
*
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
@FXML
|
|
||||||
private void sendRequest() {
|
|
||||||
final var text = searchBar.getTextField().getText().strip();
|
|
||||||
if (!text.isBlank()) eventBus.dispatch(new SendEvent(new ContactSearchRequest(text)));
|
|
||||||
else chatList.getItems().clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the text in the search bar and the items shown in the list.
|
|
||||||
* Additionally disables both clear and search button.
|
|
||||||
*
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
@FXML
|
|
||||||
private void clear() {
|
|
||||||
searchBar.getTextField().setText(null);
|
|
||||||
chatList.getItems().clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an {@link ContactOperation} for every selected contact to the
|
|
||||||
* server.
|
|
||||||
*
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
@FXML
|
|
||||||
private void chatListClicked() {
|
|
||||||
final var chat = chatList.getSelectionModel().getSelectedItem();
|
|
||||||
if (chat != null) {
|
|
||||||
final var alert = new Alert(AlertType.CONFIRMATION);
|
|
||||||
alert.setTitle("Add Contact to Contact List");
|
|
||||||
alert.setHeaderText("Add the user " + chat.getRecipient().getName() + " to your contact list?");
|
|
||||||
// Normally, this would be total BS (we are already on the FX Thread), however
|
|
||||||
// it could be proven that the creation of this dialog wrapped in
|
|
||||||
// Platform.runLater is less error-prone than without it
|
|
||||||
Platform.runLater(() -> alert.showAndWait().filter(btn -> btn == ButtonType.OK).ifPresent(btn -> {
|
|
||||||
final var event = new ContactOperation(chat.getRecipient(), ElementOperation.ADD);
|
|
||||||
// Sends the event to the server
|
|
||||||
eventBus.dispatch(new SendEvent(event));
|
|
||||||
// Updates the UI
|
|
||||||
eventBus.dispatch(event);
|
|
||||||
logger.log(Level.INFO, "Added contact " + chat.getRecipient());
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void newGroupButtonClicked() {
|
|
||||||
sceneContext.load(SceneContext.SceneInfo.GROUP_CREATION_SCENE);
|
|
||||||
sceneContext.<GroupCreationScene>getController().initializeData(sceneContext, localDB);
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void backButtonClicked() { sceneContext.pop(); }
|
|
||||||
}
|
|
@ -0,0 +1,128 @@
|
|||||||
|
package envoy.client.ui.controller;
|
||||||
|
|
||||||
|
import java.util.logging.*;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.control.Alert.AlertType;
|
||||||
|
|
||||||
|
import envoy.client.data.Context;
|
||||||
|
import envoy.client.event.BackEvent;
|
||||||
|
import envoy.client.helper.AlertHelper;
|
||||||
|
import envoy.client.net.Client;
|
||||||
|
import envoy.client.ui.control.ContactControl;
|
||||||
|
import envoy.client.ui.listcell.ListCellFactory;
|
||||||
|
import envoy.data.User;
|
||||||
|
import envoy.event.ElementOperation;
|
||||||
|
import envoy.event.contact.*;
|
||||||
|
import envoy.util.EnvoyLog;
|
||||||
|
|
||||||
|
import dev.kske.eventbus.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a search bar in which a user name (substring) can be entered. The
|
||||||
|
* users with a matching name are then displayed inside a list view. A
|
||||||
|
* {@link UserSearchRequest} is sent on every keystroke.
|
||||||
|
* <p>
|
||||||
|
* <i>The actual search algorithm is implemented on the server.
|
||||||
|
* <p>
|
||||||
|
* To create a group, a button is available that loads the
|
||||||
|
* {@link GroupCreationTab}.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @author Maximilian Käfer
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public class ContactSearchTab implements EventListener {
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TextArea searchBar;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ListView<User> userList;
|
||||||
|
|
||||||
|
private User currentlySelectedUser;
|
||||||
|
|
||||||
|
private final Alert alert = new Alert(AlertType.CONFIRMATION);
|
||||||
|
|
||||||
|
private static final Client client = Context.getInstance().getClient();
|
||||||
|
private static final EventBus eventBus = EventBus.getInstance();
|
||||||
|
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void initialize() {
|
||||||
|
eventBus.registerListener(this);
|
||||||
|
userList.setCellFactory(new ListCellFactory<>(ContactControl::new));
|
||||||
|
alert.setTitle("Add User?");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Event
|
||||||
|
private void onUserSearchResult(UserSearchResult result) {
|
||||||
|
Platform.runLater(() -> { userList.getItems().clear(); userList.getItems().addAll(result.get()); });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Event
|
||||||
|
private void onContactOperation(ContactOperation operation) {
|
||||||
|
final var contact = operation.get();
|
||||||
|
if (operation.getOperationType() == ElementOperation.ADD) Platform.runLater(() -> {
|
||||||
|
userList.getItems().remove(contact);
|
||||||
|
if (currentlySelectedUser != null && currentlySelectedUser.equals(contact) && alert.isShowing()) alert.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If text is present, sends a request to the server.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void sendRequest() {
|
||||||
|
final var text = searchBar.getText().strip();
|
||||||
|
if (!text.isBlank()) client.send(new UserSearchRequest(text));
|
||||||
|
else userList.getItems().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the text in the search bar and the items shown in the list.
|
||||||
|
* Additionally disables both clear and search button.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void clear() {
|
||||||
|
searchBar.setText(null);
|
||||||
|
userList.getItems().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an {@link ContactOperation} for the selected user to the
|
||||||
|
* server.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void userListClicked() {
|
||||||
|
final var user = userList.getSelectionModel().getSelectedItem();
|
||||||
|
if (user != null) {
|
||||||
|
currentlySelectedUser = user;
|
||||||
|
alert.setContentText("Add user " + currentlySelectedUser.getName() + " to your contacts?");
|
||||||
|
AlertHelper.confirmAction(alert, this::addAsContact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAsContact() {
|
||||||
|
|
||||||
|
// Sends the event to the server
|
||||||
|
final var event = new ContactOperation(currentlySelectedUser, ElementOperation.ADD);
|
||||||
|
client.send(event);
|
||||||
|
|
||||||
|
// Removes the chosen user and updates the UI
|
||||||
|
userList.getItems().remove(currentlySelectedUser);
|
||||||
|
eventBus.dispatch(event);
|
||||||
|
logger.log(Level.INFO, "Added user " + currentlySelectedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void backButtonClicked() { eventBus.dispatch(new BackEvent()); }
|
||||||
|
}
|
@ -1,109 +0,0 @@
|
|||||||
package envoy.client.ui.controller;
|
|
||||||
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.fxml.FXML;
|
|
||||||
import javafx.scene.control.*;
|
|
||||||
import javafx.scene.control.Alert.AlertType;
|
|
||||||
|
|
||||||
import envoy.client.data.Chat;
|
|
||||||
import envoy.client.data.LocalDB;
|
|
||||||
import envoy.client.event.SendEvent;
|
|
||||||
import envoy.client.ui.ClearableTextField;
|
|
||||||
import envoy.client.ui.SceneContext;
|
|
||||||
import envoy.client.ui.listcell.ContactListCellFactory;
|
|
||||||
import envoy.data.Group;
|
|
||||||
import envoy.event.EventBus;
|
|
||||||
import envoy.event.GroupCreation;
|
|
||||||
import envoy.util.Bounds;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>ContactSearchSceneController.java</strong><br>
|
|
||||||
* Created: <strong>07.06.2020</strong><br>
|
|
||||||
*
|
|
||||||
* @author Maximilian Käfer
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public class GroupCreationScene {
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Button createButton;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ClearableTextField groupNameField;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ListView<Chat> chatList;
|
|
||||||
|
|
||||||
private SceneContext sceneContext;
|
|
||||||
|
|
||||||
private static final EventBus eventBus = EventBus.getInstance();
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void initialize() {
|
|
||||||
chatList.setCellFactory(ContactListCellFactory::new);
|
|
||||||
chatList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
|
||||||
groupNameField.setClearButtonListener(e -> { groupNameField.getTextField().clear(); createButton.setDisable(true); });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param sceneContext enables the user to return to the chat scene
|
|
||||||
* @param localDB the local database from which potential group members can
|
|
||||||
* be selected
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public void initializeData(SceneContext sceneContext, LocalDB localDB) {
|
|
||||||
this.sceneContext = sceneContext;
|
|
||||||
Platform.runLater(() -> chatList.getItems()
|
|
||||||
.addAll(localDB.getChats()
|
|
||||||
.stream()
|
|
||||||
.filter(c -> !(c.getRecipient() instanceof Group))
|
|
||||||
.filter(c -> c.getRecipient().getID() != localDB.getUser().getID())
|
|
||||||
.collect(Collectors.toList())));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables the {@code createButton} if at least one contact is selected.
|
|
||||||
*
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
@FXML
|
|
||||||
private void chatListClicked() {
|
|
||||||
createButton.setDisable(chatList.getSelectionModel().isEmpty() || groupNameField.getTextField().getText().isBlank());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks, whether the {@code createButton} can be enabled because text is
|
|
||||||
* present in the textfield.
|
|
||||||
*
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
@FXML
|
|
||||||
private void textUpdated() { createButton.setDisable(groupNameField.getTextField().getText().isBlank()); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a {@link GroupCreation} to the server and closes this scene.
|
|
||||||
* <p>
|
|
||||||
* If the given group name is not valid, an error is displayed instead.
|
|
||||||
*
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
@FXML
|
|
||||||
private void createButtonClicked() {
|
|
||||||
final var name = groupNameField.getTextField().getText();
|
|
||||||
if (!Bounds.isValidContactName(name)) {
|
|
||||||
new Alert(AlertType.ERROR, "The entered group name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
|
|
||||||
groupNameField.getTextField().clear();
|
|
||||||
} else {
|
|
||||||
eventBus.dispatch(new SendEvent(new GroupCreation(name,
|
|
||||||
chatList.getSelectionModel().getSelectedItems().stream().map(c -> c.getRecipient().getID()).collect(Collectors.toSet()))));
|
|
||||||
new Alert(AlertType.INFORMATION, String.format("Group '%s' successfully created.", name)).showAndWait();
|
|
||||||
sceneContext.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private void backButtonClicked() { sceneContext.pop(); }
|
|
||||||
}
|
|
@ -0,0 +1,212 @@
|
|||||||
|
package envoy.client.ui.controller;
|
||||||
|
|
||||||
|
import static java.util.function.Predicate.not;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
|
||||||
|
import envoy.client.data.*;
|
||||||
|
import envoy.client.event.BackEvent;
|
||||||
|
import envoy.client.ui.control.ContactControl;
|
||||||
|
import envoy.client.ui.listcell.ListCellFactory;
|
||||||
|
import envoy.data.*;
|
||||||
|
import envoy.event.GroupCreation;
|
||||||
|
import envoy.event.contact.ContactOperation;
|
||||||
|
import envoy.util.Bounds;
|
||||||
|
|
||||||
|
import dev.kske.eventbus.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a group creation interface. A group name can be entered in the text
|
||||||
|
* field at the top. Available users (local chat recipients) are displayed
|
||||||
|
* inside a list and can be selected (multiple selection available).
|
||||||
|
* <p>
|
||||||
|
* When the group creation button is pressed, a {@link GroupCreation} is sent to
|
||||||
|
* the server. This controller enforces a valid group name and a non-empty
|
||||||
|
* member list (excluding the client user).
|
||||||
|
*
|
||||||
|
* @author Maximilian Käfer
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public class GroupCreationTab implements EventListener {
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button createButton;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button cancelButton;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TextArea groupNameField;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ListView<User> userList;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label errorMessageLabel;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button proceedDuplicateButton;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button cancelDuplicateButton;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private HBox errorProceedBox;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private final LocalDB localDB = Context.getInstance().getLocalDB();
|
||||||
|
|
||||||
|
private static final EventBus eventBus = EventBus.getInstance();
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void initialize() {
|
||||||
|
userList.setCellFactory(new ListCellFactory<>(ContactControl::new));
|
||||||
|
userList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||||
|
createButton.setDisable(true);
|
||||||
|
eventBus.registerListener(this);
|
||||||
|
userList.getItems()
|
||||||
|
.addAll(localDB.getChats()
|
||||||
|
.stream()
|
||||||
|
.map(Chat::getRecipient)
|
||||||
|
.filter(User.class::isInstance)
|
||||||
|
.filter(not(localDB.getUser()::equals))
|
||||||
|
.map(User.class::cast)
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables the {@code createButton} if at least one contact is selected.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void userListClicked() { createButton.setDisable(userList.getSelectionModel().isEmpty() || groupNameField.getText().isBlank()); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks, whether the {@code createButton} can be enabled because text is
|
||||||
|
* present in the text field.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void textUpdated() { createButton.setDisable(userList.getSelectionModel().isEmpty() || groupNameField.getText().isBlank()); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a {@link GroupCreation} to the server and closes this scene.
|
||||||
|
* <p>
|
||||||
|
* If the given group name is not valid, an error is displayed instead.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void createButtonClicked() {
|
||||||
|
name = groupNameField.getText();
|
||||||
|
if (!Bounds.isValidContactName(name)) {
|
||||||
|
setErrorMessageLabelSize(30);
|
||||||
|
errorMessageLabel.setText("The group name is not valid!");
|
||||||
|
groupNameField.clear();
|
||||||
|
} else if (groupNameAlreadyPresent(name)) {
|
||||||
|
setErrorMessageLabelSize(30);
|
||||||
|
errorMessageLabel.setText("Name does already exist! Proceed anyways?");
|
||||||
|
setProcessPaneSize(30);
|
||||||
|
createButton.setDisable(true);
|
||||||
|
cancelButton.setDisable(true);
|
||||||
|
} else {
|
||||||
|
createGroup(name);
|
||||||
|
eventBus.dispatch(new BackEvent());
|
||||||
|
// Restoring the original design as tabs will always be reused
|
||||||
|
setErrorMessageLabelSize(0);
|
||||||
|
groupNameField.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new group with the given name and all selected members.<br>
|
||||||
|
* Additionally pops the scene automatically.
|
||||||
|
*
|
||||||
|
* @param name the chosen group name
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
private void createGroup(String name) {
|
||||||
|
Context.getInstance()
|
||||||
|
.getClient()
|
||||||
|
.send(new GroupCreation(name, userList.getSelectionModel().getSelectedItems().stream().map(User::getID).collect(Collectors.toSet())));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the proposed group name is already present in the users
|
||||||
|
* {@code LocalDB}.
|
||||||
|
*
|
||||||
|
* @param newName the chosen group name
|
||||||
|
* @return true if this name is already present
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public boolean groupNameAlreadyPresent(String newName) {
|
||||||
|
return localDB.getChats().stream().map(Chat::getRecipient).filter(Group.class::isInstance).map(Contact::getName).anyMatch(newName::equals);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void backButtonClicked() {
|
||||||
|
eventBus.dispatch(new BackEvent());
|
||||||
|
setErrorMessageLabelSize(0);
|
||||||
|
setProcessPaneSize(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void proceedOnNameDuplication() {
|
||||||
|
createButton.setDisable(false);
|
||||||
|
cancelButton.setDisable(false);
|
||||||
|
createGroup(name);
|
||||||
|
eventBus.dispatch(new BackEvent());
|
||||||
|
setErrorMessageLabelSize(0);
|
||||||
|
setProcessPaneSize(0);
|
||||||
|
groupNameField.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void cancelOnNameDuplication() {
|
||||||
|
createButton.setDisable(false);
|
||||||
|
cancelButton.setDisable(false);
|
||||||
|
setErrorMessageLabelSize(0);
|
||||||
|
setProcessPaneSize(0);
|
||||||
|
groupNameField.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setErrorMessageLabelSize(int value) {
|
||||||
|
errorMessageLabel.setPrefHeight(value);
|
||||||
|
errorMessageLabel.setMinHeight(value);
|
||||||
|
errorMessageLabel.setMaxHeight(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setProcessPaneSize(int value) {
|
||||||
|
proceedDuplicateButton.setPrefHeight(value);
|
||||||
|
proceedDuplicateButton.setMinHeight(value);
|
||||||
|
proceedDuplicateButton.setMaxHeight(value);
|
||||||
|
cancelDuplicateButton.setPrefHeight(value);
|
||||||
|
cancelDuplicateButton.setMinHeight(value);
|
||||||
|
cancelDuplicateButton.setMaxHeight(value);
|
||||||
|
errorProceedBox.setPrefHeight(value);
|
||||||
|
errorProceedBox.setMinHeight(value);
|
||||||
|
errorProceedBox.setMaxHeight(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Event
|
||||||
|
private void onContactOperation(ContactOperation operation) {
|
||||||
|
if (operation.get() instanceof User) Platform.runLater(() -> {
|
||||||
|
switch (operation.getOperationType()) {
|
||||||
|
case ADD:
|
||||||
|
userList.getItems().add((User) operation.get());
|
||||||
|
break;
|
||||||
|
case REMOVE:
|
||||||
|
userList.getItems().removeIf(operation.get()::equals);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,44 +1,35 @@
|
|||||||
package envoy.client.ui.controller;
|
package envoy.client.ui.controller;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.time.Instant;
|
||||||
import java.io.IOException;
|
import java.util.logging.*;
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.Alert.AlertType;
|
import javafx.scene.control.Alert.AlertType;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
|
||||||
import envoy.client.data.*;
|
import envoy.client.data.ClientConfig;
|
||||||
import envoy.client.net.Client;
|
import envoy.client.ui.*;
|
||||||
import envoy.client.net.WriteProxy;
|
import envoy.client.util.IconUtil;
|
||||||
import envoy.client.ui.ClearableTextField;
|
|
||||||
import envoy.client.ui.SceneContext;
|
|
||||||
import envoy.client.ui.Startup;
|
|
||||||
import envoy.data.LoginCredentials;
|
import envoy.data.LoginCredentials;
|
||||||
import envoy.data.User;
|
|
||||||
import envoy.data.User.UserStatus;
|
|
||||||
import envoy.event.EventBus;
|
|
||||||
import envoy.event.HandshakeRejection;
|
import envoy.event.HandshakeRejection;
|
||||||
import envoy.exception.EnvoyException;
|
import envoy.util.*;
|
||||||
import envoy.util.Bounds;
|
|
||||||
import envoy.util.EnvoyLog;
|
import dev.kske.eventbus.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project: <strong>envoy-client</strong><br>
|
* Controller for the login scene.
|
||||||
* File: <strong>LoginDialog.java</strong><br>
|
|
||||||
* Created: <strong>03.04.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @author Maximilian Käfer
|
* @author Maximilian Käfer
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public final class LoginScene {
|
public final class LoginScene implements EventListener {
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private ClearableTextField userTextField;
|
private TextField userTextField;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private PasswordField passwordField;
|
private PasswordField passwordField;
|
||||||
@ -47,81 +38,86 @@ public final class LoginScene {
|
|||||||
private PasswordField repeatPasswordField;
|
private PasswordField repeatPasswordField;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Label repeatPasswordLabel;
|
private Button registerSwitch;
|
||||||
|
|
||||||
@FXML
|
|
||||||
private CheckBox registerCheckBox;
|
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Label connectionLabel;
|
private Label connectionLabel;
|
||||||
|
|
||||||
private Client client;
|
@FXML
|
||||||
private LocalDB localDB;
|
private Button loginButton;
|
||||||
private CacheMap cacheMap;
|
|
||||||
private SceneContext sceneContext;
|
|
||||||
|
|
||||||
private static final Logger logger = EnvoyLog.getLogger(LoginScene.class);
|
@FXML
|
||||||
private static final EventBus eventBus = EventBus.getInstance();
|
private CheckBox cbStaySignedIn;
|
||||||
private static final ClientConfig config = ClientConfig.getInstance();
|
|
||||||
|
@FXML
|
||||||
|
private Button offlineModeButton;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label registerTextLabel;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ImageView logo;
|
||||||
|
|
||||||
|
private boolean registration;
|
||||||
|
|
||||||
|
private static final Logger logger = EnvoyLog.getLogger(LoginScene.class);
|
||||||
|
private static final ClientConfig config = ClientConfig.getInstance();
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private void initialize() {
|
private void initialize() {
|
||||||
connectionLabel.setText("Server: " + config.getServer() + ":" + config.getPort());
|
connectionLabel.setText("Server: " + config.getServer() + ":" + config.getPort());
|
||||||
|
|
||||||
// Show an alert after an unsuccessful handshake
|
// Show an alert after an unsuccessful handshake
|
||||||
eventBus.register(HandshakeRejection.class, e -> Platform.runLater(() -> { new Alert(AlertType.ERROR, e.get()).showAndWait(); }));
|
EventBus.getInstance().registerListener(this);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
logo.setImage(IconUtil.loadIcon("envoy_logo"));
|
||||||
* Loads the login dialog using the FXML file {@code LoginDialog.fxml}.
|
|
||||||
*
|
|
||||||
* @param client the client used to perform the handshake
|
|
||||||
* @param localDB the local database used for offline login
|
|
||||||
* @param cacheMap the map of all caches needed
|
|
||||||
* @param sceneContext the scene context used to initialize the chat scene
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public void initializeData(Client client, LocalDB localDB, CacheMap cacheMap, SceneContext sceneContext) {
|
|
||||||
this.client = client;
|
|
||||||
this.localDB = localDB;
|
|
||||||
this.cacheMap = cacheMap;
|
|
||||||
this.sceneContext = sceneContext;
|
|
||||||
|
|
||||||
// Prepare handshake
|
|
||||||
localDB.loadIDGenerator();
|
|
||||||
|
|
||||||
// Set initial cursor
|
// Set initial cursor
|
||||||
userTextField.requestFocus();
|
userTextField.requestFocus();
|
||||||
|
|
||||||
// Perform automatic login if configured
|
|
||||||
if (config.hasLoginCredentials()) performHandshake(config.getLoginCredentials());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private void loginButtonPressed() {
|
private void loginButtonPressed() {
|
||||||
|
final String user = userTextField.getText(), pass = passwordField.getText(), repeatPass = repeatPasswordField.getText();
|
||||||
|
final boolean requestToken = cbStaySignedIn.isSelected();
|
||||||
|
|
||||||
// Prevent registration with unequal passwords
|
// Prevent registration with unequal passwords
|
||||||
if (registerCheckBox.isSelected() && !passwordField.getText().equals(repeatPasswordField.getText())) {
|
if (registration && !pass.equals(repeatPass)) {
|
||||||
new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait();
|
new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait();
|
||||||
repeatPasswordField.clear();
|
repeatPasswordField.clear();
|
||||||
} else if (!Bounds.isValidContactName(userTextField.getTextField().getText())) {
|
} else if (!Bounds.isValidContactName(user)) {
|
||||||
new Alert(AlertType.ERROR, "The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
|
new Alert(AlertType.ERROR, "The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
|
||||||
userTextField.getTextField().clear();
|
userTextField.clear();
|
||||||
} else performHandshake(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), registerCheckBox.isSelected(),
|
} else {
|
||||||
Startup.VERSION));
|
Instant lastSync = Startup.loadLastSync(userTextField.getText());
|
||||||
|
Startup.performHandshake(registration ? LoginCredentials.registration(user, pass, requestToken, Startup.VERSION, lastSync)
|
||||||
|
: LoginCredentials.login(user, pass, requestToken, Startup.VERSION, lastSync));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private void offlineModeButtonPressed() {
|
private void offlineModeButtonPressed() { Startup.attemptOfflineMode(userTextField.getText()); }
|
||||||
attemptOfflineMode(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), false, Startup.VERSION));
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private void registerCheckboxChanged() {
|
private void registerSwitchPressed() {
|
||||||
|
|
||||||
|
// Update button text and register switch
|
||||||
|
if (!registration) {
|
||||||
|
loginButton.setText("Register");
|
||||||
|
loginButton.setPadding(new Insets(2, 116, 2, 116));
|
||||||
|
registerTextLabel.setText("Already an account?");
|
||||||
|
registerSwitch.setText("Login");
|
||||||
|
} else {
|
||||||
|
loginButton.setText("Login");
|
||||||
|
loginButton.setPadding(new Insets(2, 125, 2, 125));
|
||||||
|
registerTextLabel.setText("No account yet?");
|
||||||
|
registerSwitch.setText("Register");
|
||||||
|
}
|
||||||
|
registration = !registration;
|
||||||
|
|
||||||
// Make repeat password field and label visible / invisible
|
// Make repeat password field and label visible / invisible
|
||||||
repeatPasswordField.setVisible(registerCheckBox.isSelected());
|
repeatPasswordField.setVisible(registration);
|
||||||
repeatPasswordLabel.setVisible(registerCheckBox.isSelected());
|
offlineModeButton.setDisable(registration);
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@ -130,70 +126,6 @@ public final class LoginScene {
|
|||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void performHandshake(LoginCredentials credentials) {
|
@Event
|
||||||
try {
|
private void onHandshakeRejection(HandshakeRejection evt) { Platform.runLater(() -> new Alert(AlertType.ERROR, evt.get()).showAndWait()); }
|
||||||
client.performHandshake(credentials, cacheMap);
|
|
||||||
if (client.isOnline()) {
|
|
||||||
loadChatScene();
|
|
||||||
client.initReceiver(localDB, cacheMap);
|
|
||||||
}
|
|
||||||
} catch (IOException | InterruptedException | TimeoutException e) {
|
|
||||||
logger.log(Level.INFO, "Could not connect to server. Entering offline mode...");
|
|
||||||
attemptOfflineMode(credentials);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void attemptOfflineMode(LoginCredentials credentials) {
|
|
||||||
try {
|
|
||||||
// Try entering offline mode
|
|
||||||
localDB.loadUsers();
|
|
||||||
final User clientUser = (User) localDB.getUsers().get(credentials.getIdentifier());
|
|
||||||
if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
|
|
||||||
client.setSender(clientUser);
|
|
||||||
loadChatScene();
|
|
||||||
} catch (final Exception e) {
|
|
||||||
new Alert(AlertType.ERROR, "Client error: " + e).showAndWait();
|
|
||||||
logger.log(Level.SEVERE, "Offline mode could not be loaded: ", e);
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadChatScene() {
|
|
||||||
|
|
||||||
// Set client user in local database
|
|
||||||
localDB.setUser(client.getSender());
|
|
||||||
|
|
||||||
// Initialize chats in local database
|
|
||||||
try {
|
|
||||||
localDB.initializeUserStorage();
|
|
||||||
localDB.loadUserData();
|
|
||||||
} catch (final FileNotFoundException e) {
|
|
||||||
// The local database file has not yet been created, probably first login
|
|
||||||
} catch (final Exception e) {
|
|
||||||
new Alert(AlertType.ERROR, "Error while loading local database: " + e + "\nChats will not be stored locally.").showAndWait();
|
|
||||||
logger.log(Level.WARNING, "Could not load local database: ", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize write proxy
|
|
||||||
final var writeProxy = new WriteProxy(client, localDB);
|
|
||||||
|
|
||||||
localDB.synchronize();
|
|
||||||
|
|
||||||
if (client.isOnline()) writeProxy.flushCache();
|
|
||||||
else
|
|
||||||
// Set all contacts to offline mode
|
|
||||||
localDB.getChats()
|
|
||||||
.stream()
|
|
||||||
.map(Chat::getRecipient)
|
|
||||||
.filter(User.class::isInstance)
|
|
||||||
.map(User.class::cast)
|
|
||||||
.forEach(u -> u.setStatus(UserStatus.OFFLINE));
|
|
||||||
|
|
||||||
// Load ChatScene
|
|
||||||
sceneContext.pop();
|
|
||||||
sceneContext.getStage().setMinHeight(400);
|
|
||||||
sceneContext.getStage().setMinWidth(350);
|
|
||||||
sceneContext.load(SceneContext.SceneInfo.CHAT_SCENE);
|
|
||||||
sceneContext.<ChatScene>getController().initializeData(sceneContext, localDB, client, writeProxy);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,19 +3,17 @@ package envoy.client.ui.controller;
|
|||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
|
||||||
import envoy.client.ui.SceneContext;
|
import envoy.client.data.Context;
|
||||||
import envoy.client.ui.settings.GeneralSettingsPane;
|
import envoy.client.ui.listcell.ListCellFactory;
|
||||||
import envoy.client.ui.settings.SettingsPane;
|
import envoy.client.ui.settings.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project: <strong>envoy-client</strong><br>
|
* Controller for the settings scene.
|
||||||
* File: <strong>SettingsSceneController.java</strong><br>
|
|
||||||
* Created: <strong>10.04.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public class SettingsScene {
|
public final class SettingsScene {
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private ListView<SettingsPane> settingsList;
|
private ListView<SettingsPane> settingsList;
|
||||||
@ -23,26 +21,10 @@ public class SettingsScene {
|
|||||||
@FXML
|
@FXML
|
||||||
private TitledPane titledPane;
|
private TitledPane titledPane;
|
||||||
|
|
||||||
private SceneContext sceneContext;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param sceneContext enables the user to return to the chat scene
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public void initializeData(SceneContext sceneContext) { this.sceneContext = sceneContext; }
|
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private void initialize() {
|
private void initialize() {
|
||||||
settingsList.setCellFactory(listView -> new ListCell<>() {
|
settingsList.setCellFactory(new ListCellFactory<>(pane -> new Label(pane.getTitle())));
|
||||||
|
settingsList.getItems().addAll(new GeneralSettingsPane(), new UserSettingsPane(), new DownloadSettingsPane(), new BugReportPane());
|
||||||
@Override
|
|
||||||
protected void updateItem(SettingsPane item, boolean empty) {
|
|
||||||
super.updateItem(item, empty);
|
|
||||||
if (!empty && item != null) setGraphic(new Label(item.getTitle()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
settingsList.getItems().add(new GeneralSettingsPane());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@ -55,5 +37,5 @@ public class SettingsScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private void backButtonClicked() { sceneContext.pop(); }
|
private void backButtonClicked() { Context.getInstance().getSceneContext().pop(); }
|
||||||
}
|
}
|
||||||
|
25
client/src/main/java/envoy/client/ui/controller/Tabs.java
Normal file
25
client/src/main/java/envoy/client/ui/controller/Tabs.java
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package envoy.client.ui.controller;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides options to select different tabs.
|
||||||
|
*
|
||||||
|
* @author Maximilian Käfer
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public enum Tabs {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the {@code contact list} tab.
|
||||||
|
*/
|
||||||
|
CONTACT_LIST,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the {@code contact search} tab.
|
||||||
|
*/
|
||||||
|
CONTACT_SEARCH,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the {@code group creation} tab.
|
||||||
|
*/
|
||||||
|
GROUP_CREATION
|
||||||
|
}
|
@ -1,11 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Contains JavaFX scene controllers.
|
* Contains JavaFX scene controllers.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>package-info.java</strong><br>
|
|
||||||
* Created: <strong>08.06.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @author Maximilian Käfer
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
package envoy.client.ui.controller;
|
package envoy.client.ui.controller;
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
package envoy.client.ui.listcell;
|
||||||
|
|
||||||
|
import javafx.scene.*;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a convenience frame for list cell creation.
|
||||||
|
*
|
||||||
|
* @author Kai S. K. Engelbart
|
||||||
|
* @param <T> the type of element displayed by the list cell
|
||||||
|
* @param <U> the type of node as which the list element will be displayed
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public abstract class AbstractListCell<T, U extends Node> extends ListCell<T> {
|
||||||
|
|
||||||
|
protected ListView<? extends T> listView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param listView the list view inside of which the cell will be displayed
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public AbstractListCell(ListView<? extends T> listView) {
|
||||||
|
this.listView = listView;
|
||||||
|
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||||
|
getStyleClass().add("list-element");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final void updateItem(T item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
if (!(empty || item == null)) {
|
||||||
|
setCursor(Cursor.HAND);
|
||||||
|
setGraphic(renderItem(item));
|
||||||
|
} else {
|
||||||
|
setGraphic(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a list item to a node. This can have side effects on the list cell.
|
||||||
|
*
|
||||||
|
* @param item the item to render
|
||||||
|
* @return a node representing the item
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
protected abstract U renderItem(T item);
|
||||||
|
}
|
@ -1,58 +0,0 @@
|
|||||||
package envoy.client.ui.listcell;
|
|
||||||
|
|
||||||
import javafx.geometry.Pos;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.layout.*;
|
|
||||||
|
|
||||||
import envoy.client.data.Chat;
|
|
||||||
import envoy.data.Contact;
|
|
||||||
import envoy.data.Group;
|
|
||||||
import envoy.data.User;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class formats a single {@link Contact} into a UI component.
|
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>ContactControl.java</strong><br>
|
|
||||||
* Created: <strong>01.07.2020</strong><br>
|
|
||||||
*
|
|
||||||
* @author Leon Hofmeister
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public class ChatControl extends HBox {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param chat the chat to display
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public ChatControl(Chat chat) {
|
|
||||||
// Container with contact name
|
|
||||||
final var vBox = new VBox();
|
|
||||||
final var nameLabel = new Label(chat.getRecipient().getName());
|
|
||||||
nameLabel.setWrapText(true);
|
|
||||||
vBox.getChildren().add(nameLabel);
|
|
||||||
if (chat.getRecipient() instanceof User) {
|
|
||||||
// Online status
|
|
||||||
final var user = (User) chat.getRecipient();
|
|
||||||
final var statusLabel = new Label(user.getStatus().toString());
|
|
||||||
statusLabel.getStyleClass().add(user.getStatus().toString().toLowerCase());
|
|
||||||
vBox.getChildren().add(statusLabel);
|
|
||||||
} else // Member count
|
|
||||||
vBox.getChildren().add(new Label(((Group) chat.getRecipient()).getContacts().size() + " members"));
|
|
||||||
|
|
||||||
getChildren().add(vBox);
|
|
||||||
if (chat.getUnreadAmount() != 0) {
|
|
||||||
Region spacing = new Region();
|
|
||||||
setHgrow(spacing, Priority.ALWAYS);
|
|
||||||
getChildren().add(spacing);
|
|
||||||
final var unreadMessagesLabel = new Label(Integer.toString(chat.getUnreadAmount()));
|
|
||||||
unreadMessagesLabel.setMinSize(15, 15);
|
|
||||||
var vBox2 = new VBox();
|
|
||||||
vBox2.setAlignment(Pos.CENTER_RIGHT);
|
|
||||||
unreadMessagesLabel.setAlignment(Pos.CENTER);
|
|
||||||
unreadMessagesLabel.getStyleClass().add("unreadMessagesAmount");
|
|
||||||
vBox2.getChildren().add(unreadMessagesLabel);
|
|
||||||
getChildren().add(vBox2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
package envoy.client.ui.listcell;
|
|
||||||
|
|
||||||
import javafx.scene.control.ListCell;
|
|
||||||
import javafx.scene.control.ListView;
|
|
||||||
|
|
||||||
import envoy.client.data.Chat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>UserListCell.java</strong><br>
|
|
||||||
* Created: <strong>28.03.2020</strong><br>
|
|
||||||
*
|
|
||||||
* @author Kai S. K. Engelbart
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public class ContactListCellFactory extends ListCell<Chat> {
|
|
||||||
|
|
||||||
private final ListView<Chat> listView;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param listView the list view inside which this cell is contained
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public ContactListCellFactory(ListView<Chat> listView) { this.listView = listView; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays the name of a contact. If the contact is a user, their online status
|
|
||||||
* is displayed as well.
|
|
||||||
*
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void updateItem(Chat chat, boolean empty) {
|
|
||||||
super.updateItem(chat, empty);
|
|
||||||
if (empty || chat.getRecipient() == null) {
|
|
||||||
setText(null);
|
|
||||||
setGraphic(null);
|
|
||||||
} else {
|
|
||||||
final var control = new ChatControl(chat);
|
|
||||||
prefWidthProperty().bind(listView.widthProperty().subtract(40));
|
|
||||||
setGraphic(control);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,32 @@
|
|||||||
|
package envoy.client.ui.listcell;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic list cell rendering an item using a provided render function.
|
||||||
|
*
|
||||||
|
* @author Kai S. K. Engelbart
|
||||||
|
* @param <T> the type of element displayed by the list cell
|
||||||
|
* @param <U> the type of node as which the list element will be displayed
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public final class GenericListCell<T, U extends Node> extends AbstractListCell<T, U> {
|
||||||
|
|
||||||
|
private final Function<? super T, U> renderer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param listView the list view inside of which the cell will be displayed
|
||||||
|
* @param renderer a function converting a list item to a node
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public GenericListCell(ListView<? extends T> listView, Function<? super T, U> renderer) {
|
||||||
|
super(listView);
|
||||||
|
this.renderer = renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected U renderItem(T item) { return renderer.apply(item); }
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package envoy.client.ui.listcell;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.util.Callback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a creation mechanism for generic list cells given a list view and a
|
||||||
|
* conversion function.
|
||||||
|
*
|
||||||
|
* @author Kai S. K. Engelbart
|
||||||
|
* @param <T> the type of object to display
|
||||||
|
* @param <U> the type of node displayed
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public final class ListCellFactory<T, U extends Node> implements Callback<ListView<T>, ListCell<T>> {
|
||||||
|
|
||||||
|
private final Function<? super T, U> renderer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param renderer a function converting the type to display into a node
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public ListCellFactory(Function<? super T, U> renderer) { this.renderer = renderer; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListCell<T> call(ListView<T> listView) { return new GenericListCell<>(listView, renderer); }
|
||||||
|
}
|
@ -1,123 +0,0 @@
|
|||||||
package envoy.client.ui.listcell;
|
|
||||||
|
|
||||||
import java.awt.Toolkit;
|
|
||||||
import java.awt.datatransfer.StringSelection;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.scene.control.ContextMenu;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.control.MenuItem;
|
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import javafx.scene.image.ImageView;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
|
|
||||||
import envoy.client.ui.AudioControl;
|
|
||||||
import envoy.client.ui.IconUtil;
|
|
||||||
import envoy.data.Message;
|
|
||||||
import envoy.data.Message.MessageStatus;
|
|
||||||
import envoy.data.User;
|
|
||||||
import envoy.util.EnvoyLog;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class formats a single {@link Message} into a UI component.
|
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>MessageControl.java</strong><br>
|
|
||||||
* Created: <strong>01.07.2020</strong><br>
|
|
||||||
*
|
|
||||||
* @author Leon Hofmeister
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public class MessageControl extends Label {
|
|
||||||
|
|
||||||
private static User client;
|
|
||||||
private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
|
|
||||||
private static final Map<MessageStatus, Image> statusImages = IconUtil.loadByEnum(MessageStatus.class, 16);
|
|
||||||
|
|
||||||
private static final Logger logger = EnvoyLog.getLogger(MessageControl.class);
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param message the message that should be formatted
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public MessageControl(Message message) {
|
|
||||||
// Creating the underlying VBox and the dateLabel
|
|
||||||
final var vbox = new VBox(new Label(dateFormat.format(message.getCreationDate())));
|
|
||||||
|
|
||||||
// Creating the actions for the MenuItems
|
|
||||||
final ContextMenu contextMenu = new ContextMenu();
|
|
||||||
final MenuItem copyMenuItem = new MenuItem("Copy");
|
|
||||||
final MenuItem deleteMenuItem = new MenuItem("Delete");
|
|
||||||
final MenuItem forwardMenuItem = new MenuItem("Forward");
|
|
||||||
final MenuItem quoteMenuItem = new MenuItem("Quote");
|
|
||||||
final MenuItem infoMenuItem = new MenuItem("Info");
|
|
||||||
copyMenuItem.setOnAction(e -> copyMessage(message));
|
|
||||||
deleteMenuItem.setOnAction(e -> deleteMessage(message));
|
|
||||||
forwardMenuItem.setOnAction(e -> forwardMessage(message));
|
|
||||||
quoteMenuItem.setOnAction(e -> quoteMessage(message));
|
|
||||||
infoMenuItem.setOnAction(e -> loadMessageInfoScene(message));
|
|
||||||
contextMenu.getItems().addAll(copyMenuItem, deleteMenuItem, forwardMenuItem, quoteMenuItem, infoMenuItem);
|
|
||||||
|
|
||||||
// Handling message attachment display
|
|
||||||
if (message.hasAttachment()) {
|
|
||||||
switch (message.getAttachment().getType()) {
|
|
||||||
case PICTURE:
|
|
||||||
vbox.getChildren().add(new ImageView(new Image(new ByteArrayInputStream(message.getAttachment().getData()), 256, 256, true, true)));
|
|
||||||
break;
|
|
||||||
case VIDEO:
|
|
||||||
break;
|
|
||||||
case VOICE:
|
|
||||||
vbox.getChildren().add(new AudioControl(message.getAttachment().getData()));
|
|
||||||
break;
|
|
||||||
case DOCUMENT:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
final var saveAttachment = new MenuItem("Save attachment");
|
|
||||||
saveAttachment.setOnAction(e -> saveAttachment(message));
|
|
||||||
contextMenu.getItems().add(saveAttachment);
|
|
||||||
}
|
|
||||||
// Creating the textLabel
|
|
||||||
final var textLabel = new Label(message.getText());
|
|
||||||
textLabel.setWrapText(true);
|
|
||||||
vbox.getChildren().add(textLabel);
|
|
||||||
// Setting the message status icon and background color
|
|
||||||
if (message.getSenderID() == client.getID()) {
|
|
||||||
final var statusIcon = new ImageView(statusImages.get(message.getStatus()));
|
|
||||||
statusIcon.setPreserveRatio(true);
|
|
||||||
vbox.getChildren().add(statusIcon);
|
|
||||||
getStyleClass().add("own-message");
|
|
||||||
} else getStyleClass().add("received-message");
|
|
||||||
// Adjusting height and weight of the cell to the corresponding ListView
|
|
||||||
paddingProperty().setValue(new Insets(5, 20, 5, 20));
|
|
||||||
setContextMenu(contextMenu);
|
|
||||||
setGraphic(vbox);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Context Menu actions
|
|
||||||
|
|
||||||
private void copyMessage(Message message) {
|
|
||||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(message.getText()), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteMessage(Message message) { logger.log(Level.FINEST, "message deletion was requested for " + message); }
|
|
||||||
|
|
||||||
private void forwardMessage(Message message) { logger.log(Level.FINEST, "message forwarding was requested for " + message); }
|
|
||||||
|
|
||||||
private void quoteMessage(Message message) { logger.log(Level.FINEST, "message quotation was requested for " + message); }
|
|
||||||
|
|
||||||
private void loadMessageInfoScene(Message message) { logger.log(Level.FINEST, "message info scene was requested for " + message); }
|
|
||||||
|
|
||||||
private void saveAttachment(Message message) { logger.log(Level.FINEST, "attachment saving was requested for " + message); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param client the user who has logged in
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public static void setUser(User client) { MessageControl.client = client; }
|
|
||||||
}
|
|
@ -0,0 +1,37 @@
|
|||||||
|
package envoy.client.ui.listcell;
|
||||||
|
|
||||||
|
import javafx.geometry.*;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
|
||||||
|
import envoy.client.ui.control.MessageControl;
|
||||||
|
import envoy.data.Message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list cell containing messages represented as message controls.
|
||||||
|
*
|
||||||
|
* @author Kai S. K. Engelbart
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public final class MessageListCell extends AbstractListCell<Message, MessageControl> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param listView the list view inside of which the cell will be displayed
|
||||||
|
* @since Envoy Client v0.1-beta
|
||||||
|
*/
|
||||||
|
public MessageListCell(ListView<? extends Message> listView) { super(listView); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected MessageControl renderItem(Message message) {
|
||||||
|
final var control = new MessageControl(message);
|
||||||
|
listView.widthProperty().addListener((observable, oldValue, newValue) -> adjustPadding(newValue.intValue(), control.isOwnMessage()));
|
||||||
|
adjustPadding((int) listView.getWidth(), control.isOwnMessage());
|
||||||
|
if (control.isOwnMessage()) setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
else setAlignment(Pos.CENTER_LEFT);
|
||||||
|
return control;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void adjustPadding(int listWidth, boolean ownMessage) {
|
||||||
|
int padding = 10 + Math.max((listWidth - 1000) / 2, 0);
|
||||||
|
setPadding(ownMessage ? new Insets(0, padding, 6, 0) : new Insets(0, 0, 6, padding));
|
||||||
|
}
|
||||||
|
}
|
@ -1,52 +0,0 @@
|
|||||||
package envoy.client.ui.listcell;
|
|
||||||
|
|
||||||
import javafx.scene.control.ListCell;
|
|
||||||
import javafx.scene.control.ListView;
|
|
||||||
import javafx.scene.control.Tooltip;
|
|
||||||
import javafx.stage.PopupWindow.AnchorLocation;
|
|
||||||
|
|
||||||
import envoy.data.Message;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays a single message inside the message list.
|
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>MessageListCellFactory.java</strong><br>
|
|
||||||
* Created: <strong>28.03.2020</strong><br>
|
|
||||||
*
|
|
||||||
* @author Kai S. K. Engelbart
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public class MessageListCellFactory extends ListCell<Message> {
|
|
||||||
|
|
||||||
private final ListView<Message> listView;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param listView the list view inside which this cell is contained
|
|
||||||
* @since Envoy Client v0.1-beta
|
|
||||||
*/
|
|
||||||
public MessageListCellFactory(ListView<Message> listView) { this.listView = listView; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays the text, the data of creation and the status of a message.
|
|
||||||
*
|
|
||||||
* @since Envoy v0.1-beta
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void updateItem(Message message, boolean empty) {
|
|
||||||
super.updateItem(message, empty);
|
|
||||||
if (empty || message == null) {
|
|
||||||
setText(null);
|
|
||||||
setGraphic(null);
|
|
||||||
} else {
|
|
||||||
final var control = new MessageControl(message);
|
|
||||||
control.prefWidthProperty().bind(listView.widthProperty().subtract(40));
|
|
||||||
// Creating the Tooltip to deselect a message
|
|
||||||
final var tooltip = new Tooltip("You can select a message by clicking on it \nand deselect it by pressing \"ctrl\" and clicking on it");
|
|
||||||
tooltip.setWrapText(true);
|
|
||||||
tooltip.setAnchorLocation(AnchorLocation.WINDOW_TOP_LEFT);
|
|
||||||
setTooltip(tooltip);
|
|
||||||
setGraphic(control);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* This package contains custom list cells that are used to display certain
|
* This package contains custom list cells that are used to display certain
|
||||||
* things.
|
* things.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>package-info.java</strong><br>
|
|
||||||
* Created: <strong>30.06.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Leon Hofmeister
|
* @author Leon Hofmeister
|
||||||
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
package envoy.client.ui.listcell;
|
package envoy.client.ui.listcell;
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
package envoy.client.ui.settings;
|
||||||
|
|
||||||
|
import javafx.event.EventHandler;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.input.InputEvent;
|
||||||
|
|
||||||
|
import envoy.event.IssueProposal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class offers the option for users to submit a bug report. Only the title
|
||||||
|
* of a bug is needed to be sent.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public final class BugReportPane extends OnlineOnlySettingsPane {
|
||||||
|
|
||||||
|
private final Label titleLabel = new Label("Suggest a title for the bug:");
|
||||||
|
private final TextField titleTextField = new TextField();
|
||||||
|
private final Label pleaseExplainLabel = new Label("Paste here the log of what went wrong and/ or explain what went wrong:");
|
||||||
|
private final TextArea errorDetailArea = new TextArea();
|
||||||
|
private final CheckBox showUsernameInBugReport = new CheckBox("Show your username in the bug report?");
|
||||||
|
private final Button submitReportButton = new Button("Submit report");
|
||||||
|
|
||||||
|
private final EventHandler<? super InputEvent> inputEventHandler = e -> submitReportButton.setDisable(titleTextField.getText().isBlank());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code BugReportPane}.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public BugReportPane() {
|
||||||
|
super("Report a bug");
|
||||||
|
setSpacing(10);
|
||||||
|
setToolTipText("A bug can only be reported while being online");
|
||||||
|
|
||||||
|
// Displaying the label to ask for a title
|
||||||
|
titleLabel.setWrapText(true);
|
||||||
|
getChildren().add(titleLabel);
|
||||||
|
|
||||||
|
// Displaying the TextField where to enter the title of this bug
|
||||||
|
titleTextField.setOnKeyTyped(inputEventHandler);
|
||||||
|
titleTextField.setOnInputMethodTextChanged(inputEventHandler);
|
||||||
|
getChildren().add(titleTextField);
|
||||||
|
|
||||||
|
// Displaying the label to ask for clarification
|
||||||
|
pleaseExplainLabel.setWrapText(true);
|
||||||
|
getChildren().add(pleaseExplainLabel);
|
||||||
|
|
||||||
|
// Displaying the TextArea where to enter the log and/or own description
|
||||||
|
errorDetailArea.setWrapText(true);
|
||||||
|
getChildren().add(errorDetailArea);
|
||||||
|
|
||||||
|
// Displaying the consent button that your user name will be shown
|
||||||
|
showUsernameInBugReport.setSelected(true);
|
||||||
|
getChildren().add(showUsernameInBugReport);
|
||||||
|
|
||||||
|
// Displaying the submitReportButton
|
||||||
|
submitReportButton.setDisable(true);
|
||||||
|
submitReportButton.setOnAction(e -> {
|
||||||
|
String title = titleTextField.getText(), description = errorDetailArea.getText();
|
||||||
|
client.send(showUsernameInBugReport.isSelected() ? new IssueProposal(title, description, true) : new IssueProposal(title, description, client.getSender().getName(), true));
|
||||||
|
});
|
||||||
|
getChildren().add(submitReportButton);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
package envoy.client.ui.settings;
|
||||||
|
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.stage.DirectoryChooser;
|
||||||
|
|
||||||
|
import envoy.client.data.Context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays options for downloading {@link envoy.data.Attachment}s.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public final class DownloadSettingsPane extends SettingsPane {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code DownloadSettingsPane}.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public DownloadSettingsPane() {
|
||||||
|
super("Download");
|
||||||
|
setSpacing(15);
|
||||||
|
setPadding(new Insets(15));
|
||||||
|
|
||||||
|
// Checkbox to disable asking
|
||||||
|
final var checkBox = new CheckBox(settings.getItems().get("autoSaveDownloads").getUserFriendlyName());
|
||||||
|
checkBox.setSelected(settings.isDownloadSavedWithoutAsking());
|
||||||
|
checkBox.setTooltip(new Tooltip("Determines whether a \"Select save location\" - dialogue will be shown when saving attachments."));
|
||||||
|
checkBox.setOnAction(e -> settings.setDownloadSavedWithoutAsking(checkBox.isSelected()));
|
||||||
|
getChildren().add(checkBox);
|
||||||
|
|
||||||
|
// Displaying the default path to save to
|
||||||
|
final var pathLabel = new Label(settings.getItems().get("downloadLocation").getDescription() + ":");
|
||||||
|
pathLabel.setWrapText(true);
|
||||||
|
getChildren().add(pathLabel);
|
||||||
|
final var hbox = new HBox(20);
|
||||||
|
Tooltip.install(hbox, new Tooltip("Determines the location where attachments will be saved to."));
|
||||||
|
final var currentPath = new Label(settings.getDownloadLocation().getAbsolutePath());
|
||||||
|
hbox.getChildren().add(currentPath);
|
||||||
|
|
||||||
|
// Setting the default path
|
||||||
|
final var button = new Button("Select");
|
||||||
|
button.setOnAction(e -> {
|
||||||
|
final var directoryChooser = new DirectoryChooser();
|
||||||
|
directoryChooser.setTitle("Select the directory where attachments should be saved to");
|
||||||
|
directoryChooser.setInitialDirectory(settings.getDownloadLocation());
|
||||||
|
final var selectedDirectory = directoryChooser.showDialog(Context.getInstance().getSceneContext().getStage());
|
||||||
|
|
||||||
|
if (selectedDirectory != null) {
|
||||||
|
currentPath.setText(selectedDirectory.getAbsolutePath());
|
||||||
|
settings.setDownloadLocation(selectedDirectory);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
hbox.getChildren().add(button);
|
||||||
|
getChildren().add(hbox);
|
||||||
|
}
|
||||||
|
}
|
@ -1,57 +1,69 @@
|
|||||||
package envoy.client.ui.settings;
|
package envoy.client.ui.settings;
|
||||||
|
|
||||||
import java.util.List;
|
import javafx.scene.control.*;
|
||||||
|
|
||||||
import javafx.scene.control.ComboBox;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
|
|
||||||
import envoy.client.data.Settings;
|
|
||||||
import envoy.client.data.SettingsItem;
|
import envoy.client.data.SettingsItem;
|
||||||
import envoy.client.event.ThemeChangeEvent;
|
import envoy.client.event.ThemeChangeEvent;
|
||||||
|
import envoy.client.helper.ShutdownHelper;
|
||||||
import envoy.data.User.UserStatus;
|
import envoy.data.User.UserStatus;
|
||||||
import envoy.event.EventBus;
|
|
||||||
|
import dev.kske.eventbus.EventBus;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>GeneralSettingsPane.java</strong><br>
|
|
||||||
* Created: <strong>18.04.2020</strong><br>
|
|
||||||
*
|
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public class GeneralSettingsPane extends SettingsPane {
|
public final class GeneralSettingsPane extends SettingsPane {
|
||||||
|
|
||||||
private static final Settings settings = Settings.getInstance();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public GeneralSettingsPane() {
|
public GeneralSettingsPane() {
|
||||||
super("General");
|
super("General");
|
||||||
final var vbox = new VBox();
|
setSpacing(10);
|
||||||
|
|
||||||
// TODO: Support other value types
|
// TODO: Support other value types
|
||||||
List.of("onCloseMode", "enterToSend")
|
final var settingsItems = settings.getItems();
|
||||||
.stream()
|
final var hideOnCloseCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("hideOnClose"));
|
||||||
.map(settings.getItems()::get)
|
final var hideOnCloseTooltip = new Tooltip("If selected, Envoy will still be present in the task bar when closed.");
|
||||||
.map(i -> new SettingsCheckbox((SettingsItem<Boolean>) i))
|
hideOnCloseTooltip.setWrapText(true);
|
||||||
.forEach(vbox.getChildren()::add);
|
hideOnCloseCheckbox.setTooltip(hideOnCloseTooltip);
|
||||||
|
getChildren().add(hideOnCloseCheckbox);
|
||||||
|
|
||||||
|
final var enterToSendCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("enterToSend"));
|
||||||
|
final var enterToSendTooltip = new Tooltip(
|
||||||
|
"When selected, messages can be sent pressing \"Enter\". A line break can be inserted by pressing \"Ctrl\" + \"Enter\". Else it will be the other way around.");
|
||||||
|
enterToSendTooltip.setWrapText(true);
|
||||||
|
enterToSendCheckbox.setTooltip(enterToSendTooltip);
|
||||||
|
getChildren().add(enterToSendCheckbox);
|
||||||
|
|
||||||
|
final var askForConfirmationCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("askForConfirmation"));
|
||||||
|
final var askForConfirmationTooltip = new Tooltip("When selected, nothing will prompt a confirmation dialog");
|
||||||
|
askForConfirmationTooltip.setWrapText(true);
|
||||||
|
askForConfirmationCheckbox.setTooltip(askForConfirmationTooltip);
|
||||||
|
getChildren().add(askForConfirmationCheckbox);
|
||||||
|
|
||||||
final var combobox = new ComboBox<String>();
|
final var combobox = new ComboBox<String>();
|
||||||
combobox.getItems().add("dark");
|
combobox.getItems().add("dark");
|
||||||
combobox.getItems().add("light");
|
combobox.getItems().add("light");
|
||||||
|
combobox.setTooltip(new Tooltip("Determines the current theme Envoy will be displayed in."));
|
||||||
combobox.setValue(settings.getCurrentTheme());
|
combobox.setValue(settings.getCurrentTheme());
|
||||||
combobox.setOnAction(
|
combobox.setOnAction(e -> { settings.setCurrentTheme(combobox.getValue()); EventBus.getInstance().dispatch(new ThemeChangeEvent()); });
|
||||||
e -> { settings.setCurrentTheme(combobox.getValue()); EventBus.getInstance().dispatch(new ThemeChangeEvent(combobox.getValue())); });
|
getChildren().add(combobox);
|
||||||
vbox.getChildren().add(combobox);
|
|
||||||
|
|
||||||
final var statusComboBox = new ComboBox<UserStatus>();
|
final var statusComboBox = new ComboBox<UserStatus>();
|
||||||
statusComboBox.getItems().setAll(UserStatus.values());
|
statusComboBox.getItems().setAll(UserStatus.values());
|
||||||
statusComboBox.setValue(UserStatus.ONLINE);
|
statusComboBox.setValue(UserStatus.ONLINE);
|
||||||
|
statusComboBox.setTooltip(new Tooltip("Change your current status"));
|
||||||
// TODO add action when value is changed
|
// TODO add action when value is changed
|
||||||
statusComboBox.setOnAction(e -> {});
|
statusComboBox.setOnAction(e -> {});
|
||||||
vbox.getChildren().add(statusComboBox);
|
getChildren().add(statusComboBox);
|
||||||
|
|
||||||
getChildren().add(vbox);
|
final var logoutButton = new Button("Logout");
|
||||||
|
logoutButton.setOnAction(e -> ShutdownHelper.logout());
|
||||||
|
final var logoutTooltip = new Tooltip("Brings you back to the login screen and removes \"remember me\" status from this account");
|
||||||
|
logoutTooltip.setWrapText(true);
|
||||||
|
logoutButton.setTooltip(logoutTooltip);
|
||||||
|
getChildren().add(logoutButton);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
package envoy.client.ui.settings;
|
||||||
|
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
|
|
||||||
|
import envoy.client.data.Context;
|
||||||
|
import envoy.client.net.Client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inheriting from this class signifies that options should only be available if
|
||||||
|
* the {@link envoy.data.User} is currently online. If the user is currently
|
||||||
|
* offline, all {@link javafx.scene.Node} variables will be disabled and a
|
||||||
|
* {@link Tooltip} will be displayed for the whole node.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @author Kai S. K. Engelbart
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public abstract class OnlineOnlySettingsPane extends SettingsPane {
|
||||||
|
|
||||||
|
protected final Client client = Context.getInstance().getClient();
|
||||||
|
|
||||||
|
private final Tooltip beOnlineReminder = new Tooltip("You need to be online to modify your account.");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param title the title of this pane
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
protected OnlineOnlySettingsPane(String title) {
|
||||||
|
super(title);
|
||||||
|
|
||||||
|
setDisable(!client.isOnline());
|
||||||
|
|
||||||
|
if (!client.isOnline()) {
|
||||||
|
final var infoLabel = new Label("You shall not pass!\n(... Unless you would happen to be online)");
|
||||||
|
infoLabel.setId("info-label-warning");
|
||||||
|
infoLabel.setWrapText(true);
|
||||||
|
getChildren().add(infoLabel);
|
||||||
|
setBackground(new Background(new BackgroundFill(Color.grayRgb(100, 0.3), CornerRadii.EMPTY, Insets.EMPTY)));
|
||||||
|
|
||||||
|
Tooltip.install(this, beOnlineReminder);
|
||||||
|
} else Tooltip.uninstall(this, beOnlineReminder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the text of the tooltip displayed for this pane.
|
||||||
|
*
|
||||||
|
* @param text the text to display
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
protected void setToolTipText(String text) { beOnlineReminder.setText(text); }
|
||||||
|
}
|
@ -6,10 +6,6 @@ import javafx.scene.control.CheckBox;
|
|||||||
import envoy.client.data.SettingsItem;
|
import envoy.client.data.SettingsItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>SettingsToggleButton.java</strong><br>
|
|
||||||
* Created: <strong>18.04.2020</strong><br>
|
|
||||||
*
|
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
package envoy.client.ui.settings;
|
package envoy.client.ui.settings;
|
||||||
|
|
||||||
import javafx.scene.layout.Pane;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
import envoy.client.data.Settings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>SettingsPane.java</strong><br>
|
|
||||||
* Created: <strong>18.04.2020</strong><br>
|
|
||||||
*
|
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public abstract class SettingsPane extends Pane {
|
public abstract class SettingsPane extends VBox {
|
||||||
|
|
||||||
protected String title;
|
protected String title;
|
||||||
|
|
||||||
|
protected static final Settings settings = Settings.getInstance();
|
||||||
|
|
||||||
protected SettingsPane(String title) { this.title = title; }
|
protected SettingsPane(String title) { this.title = title; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,177 @@
|
|||||||
|
package envoy.client.ui.settings;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.logging.*;
|
||||||
|
|
||||||
|
import javafx.event.EventHandler;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Cursor;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.control.Alert.AlertType;
|
||||||
|
import javafx.scene.image.*;
|
||||||
|
import javafx.scene.input.InputEvent;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.stage.FileChooser;
|
||||||
|
|
||||||
|
import envoy.client.data.Context;
|
||||||
|
import envoy.client.ui.control.ProfilePicImageView;
|
||||||
|
import envoy.client.util.IconUtil;
|
||||||
|
import envoy.event.*;
|
||||||
|
import envoy.util.*;
|
||||||
|
|
||||||
|
import dev.kske.eventbus.EventBus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public final class UserSettingsPane extends OnlineOnlySettingsPane {
|
||||||
|
|
||||||
|
private boolean profilePicChanged, usernameChanged, validPassword;
|
||||||
|
private byte[] currentImageBytes;
|
||||||
|
private String newUsername, newPassword = "";
|
||||||
|
|
||||||
|
private final ImageView profilePic = new ProfilePicImageView(null, 60);
|
||||||
|
private final TextField usernameTextField = new TextField();
|
||||||
|
private final PasswordField currentPasswordField = new PasswordField();
|
||||||
|
private final PasswordField newPasswordField = new PasswordField();
|
||||||
|
private final PasswordField repeatNewPasswordField = new PasswordField();
|
||||||
|
private final Button saveButton = new Button("Save");
|
||||||
|
|
||||||
|
private static final EventBus eventBus = EventBus.getInstance();
|
||||||
|
private static final Logger logger = EnvoyLog.getLogger(UserSettingsPane.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code UserSettingsPane}.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public UserSettingsPane() {
|
||||||
|
super("User");
|
||||||
|
setSpacing(10);
|
||||||
|
|
||||||
|
// Display of profile pic change mechanism
|
||||||
|
final var hbox = new HBox();
|
||||||
|
// TODO: display current profile pic
|
||||||
|
profilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon"));
|
||||||
|
profilePic.setCursor(Cursor.HAND);
|
||||||
|
profilePic.setFitWidth(60);
|
||||||
|
profilePic.setFitHeight(60);
|
||||||
|
profilePic.setOnMouseClicked(e -> {
|
||||||
|
if (!client.isOnline()) return;
|
||||||
|
final var pictureChooser = new FileChooser();
|
||||||
|
|
||||||
|
pictureChooser.setTitle("Select a new profile pic");
|
||||||
|
pictureChooser.setInitialDirectory(new File(System.getProperty("user.home")));
|
||||||
|
pictureChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif"));
|
||||||
|
|
||||||
|
final var file = pictureChooser.showOpenDialog(Context.getInstance().getSceneContext().getStage());
|
||||||
|
|
||||||
|
if (file != null) {
|
||||||
|
|
||||||
|
// Check max file size
|
||||||
|
// TODO: Move to config
|
||||||
|
if (file.length() > 5E6) {
|
||||||
|
new Alert(AlertType.WARNING, "The selected file exceeds the size limit of 5MB!").showAndWait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
currentImageBytes = Files.readAllBytes(file.toPath());
|
||||||
|
profilePic.setImage(new Image(new ByteArrayInputStream(currentImageBytes)));
|
||||||
|
profilePicChanged = true;
|
||||||
|
} catch (final IOException e1) {
|
||||||
|
e1.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
hbox.getChildren().add(profilePic);
|
||||||
|
|
||||||
|
// Displaying the username change mechanism
|
||||||
|
final var username = client.getSender().getName();
|
||||||
|
newUsername = username;
|
||||||
|
usernameTextField.setText(username);
|
||||||
|
final EventHandler<? super InputEvent> textChanged = e -> {
|
||||||
|
newUsername = usernameTextField.getText();
|
||||||
|
usernameChanged = newUsername != username;
|
||||||
|
};
|
||||||
|
usernameTextField.setOnInputMethodTextChanged(textChanged);
|
||||||
|
usernameTextField.setOnKeyTyped(textChanged);
|
||||||
|
hbox.getChildren().add(usernameTextField);
|
||||||
|
getChildren().add(hbox);
|
||||||
|
|
||||||
|
// "Displaying" the password change mechanism
|
||||||
|
final HBox[] passwordHBoxes = { new HBox(), new HBox(), new HBox() };
|
||||||
|
final Label[] passwordLabels = { new Label("Enter current password:"), new Label("Enter new password:"),
|
||||||
|
new Label("Repeat new password:") };
|
||||||
|
|
||||||
|
final PasswordField[] passwordFields = { currentPasswordField, newPasswordField, repeatNewPasswordField };
|
||||||
|
final EventHandler<? super InputEvent> passwordEntered = e -> {
|
||||||
|
newPassword = newPasswordField.getText();
|
||||||
|
validPassword = newPassword.equals(repeatNewPasswordField.getText())
|
||||||
|
&& !newPasswordField.getText().isBlank();
|
||||||
|
};
|
||||||
|
newPasswordField.setOnInputMethodTextChanged(passwordEntered);
|
||||||
|
newPasswordField.setOnKeyTyped(passwordEntered);
|
||||||
|
repeatNewPasswordField.setOnInputMethodTextChanged(passwordEntered);
|
||||||
|
repeatNewPasswordField.setOnKeyTyped(passwordEntered);
|
||||||
|
|
||||||
|
for (int i = 0; i < passwordHBoxes.length; i++) {
|
||||||
|
final var hBox2 = passwordHBoxes[i];
|
||||||
|
passwordLabels[i].setWrapText(true);
|
||||||
|
hBox2.getChildren().add(passwordLabels[i]);
|
||||||
|
hBox2.getChildren().add(passwordFields[i]);
|
||||||
|
getChildren().add(hBox2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Displaying the save button
|
||||||
|
saveButton.setOnAction(e -> save(client.getSender().getID(), currentPasswordField.getText()));
|
||||||
|
saveButton.setAlignment(Pos.BOTTOM_RIGHT);
|
||||||
|
getChildren().add(saveButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the given input and sends the changed input to the server
|
||||||
|
*
|
||||||
|
* @param username the new username
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
private void save(long userID, String oldPassword) {
|
||||||
|
|
||||||
|
// The profile pic was changed
|
||||||
|
if (profilePicChanged) {
|
||||||
|
final var profilePicChangeEvent = new ProfilePicChange(currentImageBytes, userID);
|
||||||
|
eventBus.dispatch(profilePicChangeEvent);
|
||||||
|
client.send(profilePicChangeEvent);
|
||||||
|
logger.log(Level.INFO, "The user just changed his profile pic.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The username was changed
|
||||||
|
final var validContactName = Bounds.isValidContactName(newUsername);
|
||||||
|
if (usernameChanged && validContactName) {
|
||||||
|
final var nameChangeEvent = new NameChange(userID, newUsername);
|
||||||
|
eventBus.dispatch(nameChangeEvent);
|
||||||
|
client.send(nameChangeEvent);
|
||||||
|
logger.log(Level.INFO, "The user just changed his name to " + newUsername + ".");
|
||||||
|
} else if (!validContactName) {
|
||||||
|
final var alert = new Alert(AlertType.ERROR);
|
||||||
|
alert.setTitle("Invalid username");
|
||||||
|
alert.setContentText("The entered username does not conform with the naming limitations: " + Bounds.CONTACT_NAME_PATTERN);
|
||||||
|
alert.showAndWait();
|
||||||
|
logger.log(Level.INFO, "An invalid username was requested.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The password was changed
|
||||||
|
if (validPassword) {
|
||||||
|
client.send(new PasswordChangeRequest(newPassword, oldPassword, userID));
|
||||||
|
logger.log(Level.INFO, "The user just tried to change his password!");
|
||||||
|
} else if (!(validPassword || newPassword.isBlank())) {
|
||||||
|
final var alert = new Alert(AlertType.ERROR);
|
||||||
|
alert.setTitle("Unequal Password");
|
||||||
|
alert.setContentText("Repeated password is unequal to the chosen new password");
|
||||||
|
alert.showAndWait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* This package contains classes used for representing the settings
|
* This package contains classes used for representing the settings
|
||||||
* visually.
|
* visually.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>package-info.java</strong><br>
|
|
||||||
* Created: <strong>19 Apr 2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Leon Hofmeister
|
* @author Leon Hofmeister
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package envoy.client.ui;
|
package envoy.client.util;
|
||||||
|
|
||||||
import java.util.EnumMap;
|
import java.awt.image.BufferedImage;
|
||||||
import java.util.EnumSet;
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
|
|
||||||
import envoy.client.data.Settings;
|
import envoy.client.data.Settings;
|
||||||
@ -12,15 +15,11 @@ import envoy.util.EnvoyLog;
|
|||||||
/**
|
/**
|
||||||
* Provides static utility methods for loading icons from the resource
|
* Provides static utility methods for loading icons from the resource
|
||||||
* folder.
|
* folder.
|
||||||
* <p>
|
|
||||||
* Project: <strong>envoy-client</strong><br>
|
|
||||||
* File: <strong>IconUtil.java</strong><br>
|
|
||||||
* Created: <strong>16.03.2020</strong><br>
|
|
||||||
*
|
*
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public class IconUtil {
|
public final class IconUtil {
|
||||||
|
|
||||||
private IconUtil() {}
|
private IconUtil() {}
|
||||||
|
|
||||||
@ -31,15 +30,7 @@ public class IconUtil {
|
|||||||
* @return the loaded image
|
* @return the loaded image
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public static Image load(String path) {
|
public static Image load(String path) { return new Image(IconUtil.class.getResource(path).toExternalForm()); }
|
||||||
Image image = null;
|
|
||||||
try {
|
|
||||||
image = new Image(IconUtil.class.getResource(path).toExternalForm());
|
|
||||||
} catch (final NullPointerException e) {
|
|
||||||
EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
|
|
||||||
}
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads an image from the resource folder and scales it to the given size.
|
* Loads an image from the resource folder and scales it to the given size.
|
||||||
@ -50,13 +41,7 @@ public class IconUtil {
|
|||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public static Image load(String path, int size) {
|
public static Image load(String path, int size) {
|
||||||
Image image = null;
|
return new Image(IconUtil.class.getResource(path).toExternalForm(), size, size, true, true);
|
||||||
try {
|
|
||||||
image = new Image(IconUtil.class.getResource(path).toExternalForm(), size, size, true, true);
|
|
||||||
} catch (final NullPointerException e) {
|
|
||||||
EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
|
|
||||||
}
|
|
||||||
return image;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -145,6 +130,23 @@ public class IconUtil {
|
|||||||
return icons;
|
return icons;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a buffered image from the resource folder which is compatible with AWT.
|
||||||
|
*
|
||||||
|
* @param path the path to the icon inside the resource folder
|
||||||
|
* @return the loaded image
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public static BufferedImage loadAWTCompatible(String path) {
|
||||||
|
BufferedImage image = null;
|
||||||
|
try {
|
||||||
|
image = ImageIO.read(IconUtil.class.getResource(path));
|
||||||
|
} catch (final IOException e) {
|
||||||
|
EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
|
||||||
|
}
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method should be called if the display of an image depends upon the
|
* This method should be called if the display of an image depends upon the
|
||||||
* currently active theme.<br>
|
* currently active theme.<br>
|
||||||
@ -154,7 +156,7 @@ public class IconUtil {
|
|||||||
* @return the theme specific folder
|
* @return the theme specific folder
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public static String themeSpecificSubFolder() {
|
private static String themeSpecificSubFolder() {
|
||||||
return Settings.getInstance().isUsingDefaultTheme() ? Settings.getInstance().getCurrentTheme() + "/" : "";
|
return Settings.getInstance().isUsingDefaultTheme() ? Settings.getInstance().getCurrentTheme() + "/" : "";
|
||||||
}
|
}
|
||||||
}
|
}
|
80
client/src/main/java/envoy/client/util/ReflectionUtil.java
Normal file
80
client/src/main/java/envoy/client/util/ReflectionUtil.java
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package envoy.client.util;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.*;
|
||||||
|
|
||||||
|
import javafx.scene.Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public final class ReflectionUtil {
|
||||||
|
|
||||||
|
private ReflectionUtil() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all declared variable values of the given instance that have the
|
||||||
|
* specified class.
|
||||||
|
* <p>
|
||||||
|
* (i.e. can get all {@code JComponents} (Swing) or {@code Nodes} (JavaFX) in a
|
||||||
|
* GUI class).
|
||||||
|
* <p>
|
||||||
|
* <b>Important: If you are using a module, you first need to declare <br>
|
||||||
|
* "opens {your_package} to envoy.client.util;" in your module-info.java</b>.
|
||||||
|
*
|
||||||
|
* @param <T> the type of the object
|
||||||
|
* @param <R> the type to return
|
||||||
|
* @param instance the instance of a given class whose values are to be
|
||||||
|
* evaluated
|
||||||
|
* @param typeToReturn the type of variable to return
|
||||||
|
* @return all variables in the given instance that have the requested type
|
||||||
|
* @throws RuntimeException if an exception occurs
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public static <T, R> Stream<R> getAllDeclaredVariablesOfTypeAsStream(T instance, Class<R> typeToReturn) {
|
||||||
|
return Arrays.stream(instance.getClass().getDeclaredFields()).filter(field -> typeToReturn.isAssignableFrom(field.getType())).map(field -> {
|
||||||
|
try {
|
||||||
|
field.setAccessible(true);
|
||||||
|
return typeToReturn.cast(field.get(instance));
|
||||||
|
} catch (IllegalArgumentException | IllegalAccessException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all declared variables of the given instance that are children of
|
||||||
|
* {@code Node}.
|
||||||
|
* <p>
|
||||||
|
* <b>Important: If you are using a module, you first need to declare <br>
|
||||||
|
* "opens {your_package} to envoy.client.util;" in your module-info.java</b>.
|
||||||
|
*
|
||||||
|
* @param <T> the type of the instance
|
||||||
|
* @param instance the instance of a given class whose values are to be
|
||||||
|
* evaluated
|
||||||
|
* @return all variables of the given object that have the requested type as
|
||||||
|
* {@code Stream}
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public static <T> Stream<Node> getAllDeclaredNodeVariablesAsStream(T instance) {
|
||||||
|
return getAllDeclaredVariablesOfTypeAsStream(instance, Node.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all declared variables of the given instance that are children of
|
||||||
|
* {@code Node}<br>
|
||||||
|
* <p>
|
||||||
|
* <b>Important: If you are using a module, you first need to declare <br>
|
||||||
|
* "opens {your_package} to envoy.client.util;" in your module-info.java</b>.
|
||||||
|
*
|
||||||
|
* @param <T> the type of the instance
|
||||||
|
* @param instance the instance of a given class whose values are to be
|
||||||
|
* evaluated
|
||||||
|
* @return all variables of the given object that have the requested type
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public static <T> List<Node> getAllDeclaredNodeVariables(T instance) {
|
||||||
|
return getAllDeclaredNodeVariablesAsStream(instance).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
7
client/src/main/java/envoy/client/util/package-info.java
Normal file
7
client/src/main/java/envoy/client/util/package-info.java
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* This package contains utility classes for use in envoy-client.
|
||||||
|
*
|
||||||
|
* @author Leon Hofmeister
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
package envoy.client.util;
|
@ -7,17 +7,21 @@
|
|||||||
* @author Maximilian Käfer
|
* @author Maximilian Käfer
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
module envoy {
|
module envoy.client {
|
||||||
|
|
||||||
requires transitive envoy.common;
|
requires envoy.common;
|
||||||
requires transitive java.desktop;
|
requires java.desktop;
|
||||||
requires transitive java.logging;
|
requires java.logging;
|
||||||
requires transitive java.prefs;
|
requires java.prefs;
|
||||||
requires javafx.controls;
|
requires javafx.controls;
|
||||||
requires javafx.fxml;
|
requires javafx.fxml;
|
||||||
requires javafx.base;
|
requires javafx.base;
|
||||||
requires javafx.graphics;
|
requires javafx.graphics;
|
||||||
|
|
||||||
opens envoy.client.ui to javafx.graphics, javafx.fxml;
|
opens envoy.client.ui to javafx.graphics, javafx.fxml, dev.kske.eventbus;
|
||||||
opens envoy.client.ui.controller to javafx.graphics, javafx.fxml;
|
opens envoy.client.ui.controller to javafx.graphics, javafx.fxml, envoy.client.util, dev.kske.eventbus;
|
||||||
|
opens envoy.client.ui.control to javafx.graphics, javafx.fxml;
|
||||||
|
opens envoy.client.ui.settings to envoy.client.util;
|
||||||
|
opens envoy.client.net to dev.kske.eventbus;
|
||||||
|
opens envoy.client.data to dev.kske.eventbus;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
server=localhost
|
server=localhost
|
||||||
port=8080
|
port=8080
|
||||||
localDB=.\\localDB
|
localDB=localDB
|
||||||
|
localDBSaveInterval=2
|
||||||
|
consoleLevelBarrier=FINER
|
||||||
|
fileLevelBarrier=OFF
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
.button, .list-cell, .progress-bar * {
|
.button, .list-cell, .progress-bar * {
|
||||||
-fx-background-radius: 5.0em;
|
-fx-background-radius: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-menu, .context-menu > * {
|
.context-menu, .context-menu > * {
|
||||||
@ -8,6 +8,28 @@
|
|||||||
-fx-background-color: transparent;
|
-fx-background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#text-enter-container, #contact-search-enter-container {
|
||||||
|
-fx-background-radius: 5.0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#round-button {
|
||||||
|
-fx-background-radius: 5.0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-area {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-area .scroll-pane {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
.text-area .scroll-pane .viewport{
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
.text-area .scroll-pane .content{
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.menu-item {
|
.menu-item {
|
||||||
-fx-background-radius: 15.0px;
|
-fx-background-radius: 15.0px;
|
||||||
}
|
}
|
||||||
@ -48,40 +70,75 @@
|
|||||||
|
|
||||||
.received-message {
|
.received-message {
|
||||||
-fx-alignment: center-left;
|
-fx-alignment: center-left;
|
||||||
-fx-background-radius: 4.0em;
|
-fx-background-radius: 1.3em;
|
||||||
-fx-text-alignment: right;
|
-fx-text-alignment: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.own-message {
|
.own-message {
|
||||||
-fx-alignment: center-right;
|
-fx-alignment: center-right;
|
||||||
-fx-background-radius: 4.0em;
|
-fx-background-radius: 1.3em;
|
||||||
-fx-text-alignment: left;
|
-fx-text-alignment: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unreadMessagesAmount {
|
.unread-messages-amount {
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
-fx-background-color: orange;
|
-fx-background-color: orange;
|
||||||
-fx-background-radius: 4.0em;
|
-fx-background-radius: 4.0em;
|
||||||
-fx-text-alignment: center;
|
-fx-text-alignment: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#remainingCharsLabel {
|
#login-button {
|
||||||
|
-fx-background-radius: 1.0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#register-switch {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-text-fill: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-input-field {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-border: solid;
|
||||||
|
-fx-border-width: 0.0 0.0 1.0 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#remaining-chars-label {
|
||||||
-fx-text-fill: #00FF00;
|
-fx-text-fill: #00FF00;
|
||||||
-fx-background-color: transparent;
|
-fx-background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
#infoLabel-success {
|
#info-label-success {
|
||||||
-fx-text-fill: #00FF00;
|
-fx-text-fill: #00FF00;
|
||||||
}
|
}
|
||||||
|
|
||||||
#infoLabel-info {
|
#info-label-info {
|
||||||
-fx-text-fill: yellow;
|
-fx-text-fill: yellow;
|
||||||
}
|
}
|
||||||
|
|
||||||
#infoLabel-warning {
|
#info-label-warning {
|
||||||
-fx-text-fill: orange;
|
-fx-text-fill: orange;
|
||||||
}
|
}
|
||||||
|
|
||||||
#infoLabel-error {
|
#info-label-error {
|
||||||
-fx-text-fill: red;
|
-fx-text-fill: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#transparent-background {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-pic {
|
||||||
|
-fx-radius: 1.0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-element {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane {
|
||||||
|
-fx-tab-max-height: 0.0 ;
|
||||||
|
}
|
||||||
|
.tab-pane .tab-header-area {
|
||||||
|
visibility: hidden ;
|
||||||
|
-fx-padding: -20.0 0.0 0.0 0.0;
|
||||||
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
-fx-background-color: rgb(105.0,0.0,153.0);
|
-fx-background-color: #690099;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:pressed {
|
.button:pressed {
|
||||||
@ -18,12 +18,12 @@
|
|||||||
-fx-background-color: lightgray;
|
-fx-background-color: lightgray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-view, .list-cell, .text-area .content, .text-field, .password-field, .tooltip, .pane, .pane .content, .vbox, .titled-pane > .title, .titled-pane > *.content, .context-menu, .menu-item {
|
#message-list, .text-field, .password-field, .tooltip, .pane, .pane .content, .vbox, .titled-pane > .title, .titled-pane > *.content, .context-menu, .menu-item {
|
||||||
-fx-background-color: dimgray;
|
-fx-background-color: #222222;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-cell:selected, .list-cell:selected > *, .menu-item:hover {
|
.list-cell:selected, .list-cell:selected > *, .menu-item:hover {
|
||||||
-fx-background-color: rgb(105.0,0.0,153.0);
|
-fx-background-color: #690099;
|
||||||
}
|
}
|
||||||
|
|
||||||
.received-message {
|
.received-message {
|
||||||
@ -37,3 +37,53 @@
|
|||||||
.alert.information.dialog-pane, .alert.warning.dialog-pane, .alert.error.dialog-pane {
|
.alert.information.dialog-pane, .alert.warning.dialog-pane, .alert.error.dialog-pane {
|
||||||
-fx-background-color: black;
|
-fx-background-color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#login-input-field {
|
||||||
|
-fx-border-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-background {
|
||||||
|
-fx-background-color: #191919;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-list, #top-bar, #search-panel, #note-background {
|
||||||
|
-fx-background-color: #303030;
|
||||||
|
}
|
||||||
|
|
||||||
|
#text-enter-container {
|
||||||
|
-fx-background-color: #363636;
|
||||||
|
}
|
||||||
|
|
||||||
|
#contact-search-enter-container {
|
||||||
|
-fx-background-color: #202020;
|
||||||
|
}
|
||||||
|
|
||||||
|
#underline {
|
||||||
|
-fx-border: solid;
|
||||||
|
-fx-border-width: 0.0 0.0 1.0 0.0;
|
||||||
|
-fx-border-color: #202020;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-member-names {
|
||||||
|
-fx-text-fill: #690099;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-bar:vertical, .scroll-bar:vertical .track, .scroll-bar:vertical .increment-button , .scroll-bar:vertical .decrement-button {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-bar:vertical .thumb {
|
||||||
|
-fx-background-color: #707070;
|
||||||
|
-fx-background-insets : 4.0, 0.0, 0.0;
|
||||||
|
-fx-background-radius : 2.0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#proceed-button {
|
||||||
|
-fx-text-fill: white;
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
@ -1,16 +1,85 @@
|
|||||||
.button{
|
.root {
|
||||||
-fx-background-color: orangered;
|
-fx-background-color: #E6E6E6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-cell:selected, .list-cell:selected > * {
|
.button {
|
||||||
-fx-background-color: orangered;
|
-fx-background-color: #B37D7D;
|
||||||
-fx-text-fill: black;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.received-message, .menu-item {
|
.button:pressed {
|
||||||
|
-fx-background-color: #808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
-fx-background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-list, .text-field, .password-field, .tooltip, .pane, .pane .content, .vbox, .titled-pane > .title, .titled-pane > *.content, .context-menu, .menu-item {
|
||||||
|
-fx-background-color: #E3E3E3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-cell:selected, .list-cell:selected > *, .menu-item:hover {
|
||||||
|
-fx-background-color: #805959;
|
||||||
|
}
|
||||||
|
|
||||||
|
.received-message {
|
||||||
-fx-background-color: lightgray;
|
-fx-background-color: lightgray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.own-message {
|
.own-message {
|
||||||
-fx-background-color: lightgreen;
|
-fx-background-color: #8FA88F;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.information.dialog-pane, .alert.warning.dialog-pane, .alert.error.dialog-pane {
|
||||||
|
-fx-background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-input-field {
|
||||||
|
-fx-border-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-background {
|
||||||
|
-fx-background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-list, #top-bar, #search-panel, #note-background {
|
||||||
|
-fx-background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#text-enter-container {
|
||||||
|
-fx-background-color: #F2F2F2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#contact-search-enter-container {
|
||||||
|
-fx-background-color: #CCCCCC;
|
||||||
|
}
|
||||||
|
|
||||||
|
#underline {
|
||||||
|
-fx-border: solid;
|
||||||
|
-fx-border-width: 0.0 0.0 1.0 0.0;
|
||||||
|
-fx-border-color: #CCCCCC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-member-names {
|
||||||
|
-fx-text-fill: #805959;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-bar:vertical, .scroll-bar:vertical .track, .scroll-bar:vertical .increment-button , .scroll-bar:vertical .decrement-button {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-bar:vertical .thumb {
|
||||||
|
-fx-background-color: #707070;
|
||||||
|
-fx-background-insets : 4.0, 0.0, 0.0;
|
||||||
|
-fx-background-radius : 2.0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#proceed-button {
|
||||||
|
-fx-text-fill: white;
|
||||||
|
-fx-background-color: transparent;
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user