Move Envoy Client to client/ subdirectory

This commit is contained in:
2020-07-13 11:37:45 +02:00
parent 0c4d807e41
commit 0309d0d860
105 changed files with 0 additions and 0 deletions

View File

@ -0,0 +1,570 @@
package envoy.client.ui.controller;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.animation.RotateTransition;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.stage.FileChooser;
import javafx.util.Duration;
import envoy.client.data.*;
import envoy.client.data.audio.AudioRecorder;
import envoy.client.event.MessageCreationEvent;
import envoy.client.net.Client;
import envoy.client.net.WriteProxy;
import envoy.client.ui.IconUtil;
import envoy.client.ui.Restorable;
import envoy.client.ui.SceneContext;
import envoy.client.ui.listcell.ContactListCellFactory;
import envoy.client.ui.listcell.MessageControl;
import envoy.client.ui.listcell.MessageListCellFactory;
import envoy.data.*;
import envoy.data.Attachment.AttachmentType;
import envoy.event.*;
import envoy.event.contact.ContactOperation;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>ChatSceneController.java</strong><br>
* Created: <strong>26.03.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public final class ChatScene implements Restorable {
@FXML
private GridPane scene;
@FXML
private Label contactLabel;
@FXML
private ListView<Message> messageList;
@FXML
private ListView<Chat> chatList;
@FXML
private Button postButton;
@FXML
private Button voiceButton;
@FXML
private Button attachmentButton;
@FXML
private Button settingsButton;
@FXML
private Button rotateButton;
@FXML
private TextArea messageTextArea;
@FXML
private Label remainingChars;
@FXML
private Label infoLabel;
@FXML
private MenuItem deleteContactMenuItem;
@FXML
private ImageView attachmentView;
private LocalDB localDB;
private Client client;
private WriteProxy writeProxy;
private SceneContext sceneContext;
private Chat currentChat;
private AudioRecorder recorder;
private boolean recording;
private Attachment pendingAttachment;
private boolean postingPermanentlyDisabled;
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 Image DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20);
private static final int MAX_MESSAGE_LENGTH = 255;
private static final int DEFAULT_ICON_SIZE = 16;
/**
* Initializes the appearance of certain visual components.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void initialize() {
// Initialize message and user rendering
messageList.setCellFactory(MessageListCellFactory::new);
chatList.setCellFactory(ContactListCellFactory::new);
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)));
attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
rotateButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("rotate", (int) (DEFAULT_ICON_SIZE * 1.5))));
// Listen to received messages
eventBus.register(MessageCreationEvent.class, e -> {
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
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 -> {
((GroupMessage) groupMessage).getMemberStatuses().replace(e.getMemberID(), e.get());
// Update UI if in current chat
if (currentChat != null && groupMessage.getRecipientID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh);
}));
// Listen to user status changes
eventBus.register(UserStatusChange.class,
e -> chatList.getItems()
.stream()
.filter(c -> c.getRecipient().getID() == e.getID())
.findAny()
.map(Chat::getRecipient)
.ifPresent(u -> { ((User) u).setStatus(e.get()); Platform.runLater(chatList::refresh); }));
// 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;
}
});
}
/**
* Initializes all necessary data via dependency injection-
*
* @param sceneContext the scene context used to load other scenes
* @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) {
this.sceneContext = sceneContext;
this.localDB = localDB;
this.client = client;
this.writeProxy = writeProxy;
chatList.setItems(FXCollections.observableList(localDB.getChats()));
contactLabel.setText(localDB.getUser().getName());
MessageControl.setUser(localDB.getUser());
if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info");
recorder = new AudioRecorder();
}
@Override
public void onRestore() { updateRemainingCharsLabel(); }
/**
* Actions to perform when the list of contacts has been clicked.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void chatListClicked() {
final Contact user = chatList.getSelectionModel().getSelectedItem().getRecipient();
if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) {
// LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes
// Load the chat
currentChat = localDB.getChat(user.getID()).get();
messageList.setItems(FXCollections.observableList(currentChat.getMessages()));
final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount() - 1;
messageList.scrollTo(scrollIndex);
logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex);
deleteContactMenuItem.setText("Delete " + user.getName());
// Read the current chat
try {
currentChat.read(writeProxy);
} catch (final IOException e) {
logger.log(Level.WARNING, "Could not read current chat.", e);
}
// Discard the pending attachment
if (recorder.isRecording()) {
recorder.cancel();
recording = false;
}
pendingAttachment = null;
updateAttachmentView(false);
remainingChars.setVisible(true);
remainingChars
.setText(String.format("remaining chars: %d/%d", MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH));
}
messageTextArea.setDisable(currentChat == null || postingPermanentlyDisabled);
voiceButton.setDisable(!recorder.isSupported());
attachmentButton.setDisable(false);
chatList.refresh();
}
/**
* Actions to perform when the Settings Button has been clicked.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void settingsButtonClicked() {
sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE);
sceneContext.<SettingsScene>getController().initializeData(sceneContext);
}
/**
* Actions to perform when the "Add Contact" - Button has been clicked.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void addContactButtonClicked() {
sceneContext.load(SceneContext.SceneInfo.CONTACT_SEARCH_SCENE);
sceneContext.<ContactSearchScene>getController().initializeData(sceneContext, localDB);
}
@FXML
private void voiceButtonClicked() {
new Thread(() -> {
try {
if (!recording) {
recording = true;
Platform.runLater(() -> {
voiceButton.setText("Recording");
voiceButton.setGraphic(new ImageView(IconUtil.loadIcon("microphone_recording", DEFAULT_ICON_SIZE)));
});
recorder.start();
} else {
pendingAttachment = new Attachment(recorder.finish(), AttachmentType.VOICE);
recording = false;
Platform.runLater(() -> {
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
voiceButton.setText(null);
checkPostConditions(false);
updateAttachmentView(true);
});
}
} catch (final EnvoyException e) {
logger.log(Level.SEVERE, "Could not record audio: ", e);
Platform.runLater(new Alert(AlertType.ERROR, "Could not record audio")::showAndWait);
}
}).start();
}
@FXML
private void attachmentButtonClicked() {
// Display file chooser
final var fileChooser = new FileChooser();
fileChooser.setTitle("Add Attachment");
fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));
fileChooser.getExtensionFilters()
.addAll(new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif"),
new FileChooser.ExtensionFilter("Videos", "*.mp4"),
new FileChooser.ExtensionFilter("All Files", "*.*"));
final var file = fileChooser.showOpenDialog(sceneContext.getStage());
if (file != null) {
// Check max file size
if (file.length() > 16E6) {
new Alert(AlertType.WARNING, "The selected file exceeds the size limit of 16MB!").showAndWait();
return;
}
// Get attachment type (default is document)
AttachmentType type = AttachmentType.DOCUMENT;
switch (fileChooser.getSelectedExtensionFilter().getDescription()) {
case "Pictures":
type = AttachmentType.PICTURE;
break;
case "Videos":
type = AttachmentType.VIDEO;
break;
}
// Create the pending attachment
try {
final var fileBytes = Files.readAllBytes(file.toPath());
pendingAttachment = new Attachment(fileBytes, type);
// Setting the preview image as image of the attachmentView
if (type == AttachmentType.PICTURE)
attachmentView.setImage(new Image(new ByteArrayInputStream(fileBytes), DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, true, true));
attachmentView.setVisible(true);
} catch (final IOException e) {
new Alert(AlertType.ERROR, "The selected file could not be loaded!").showAndWait();
}
}
}
/**
* Rotates every element in our application by 360° in at most 2.75s.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void doABarrelRoll() {
// contains all Node objects in ChatScene in alphabetical order
final var rotatableNodes = new Node[] { attachmentButton, attachmentView, contactLabel, infoLabel, messageList, messageTextArea,
postButton, remainingChars, rotateButton, scene, settingsButton, userList, voiceButton };
final var random = new Random();
for (final var node : rotatableNodes) {
// Defines at most four whole rotation in at most 4s
final var rotateTransition = new RotateTransition(Duration.seconds(random.nextDouble() * 3 + 1), node);
rotateTransition.setByAngle((random.nextInt(3) + 1) * 360);
rotateTransition.play();
// This is needed as for some strange reason objects could stop before being
// rotated back to 0°
rotateTransition.setOnFinished(e -> node.setRotate(0));
}
}
/**
* Checks the text length of the {@code messageTextArea}, adjusts the
* {@code remainingChars} label and checks whether to send the message
* automatically.
*
* @param e the key event that will be analyzed for a post request
* @since Envoy Client v0.1-beta
*/
@FXML
private void checkKeyCombination(KeyEvent e) {
// Checks whether the text is too long
messageTextUpdated();
// Automatic sending of messages via (ctrl +) enter
checkPostConditions(e);
}
/**
* @param e the keys that have been pressed
* @since Envoy Client v0.1-beta
*/
@FXML
private void checkPostConditions(KeyEvent e) {
checkPostConditions(settings.isEnterToSend() && e.getCode() == KeyCode.ENTER
|| !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown());
}
private void checkPostConditions(boolean sendKeyPressed) {
if (!postingPermanentlyDisabled) {
if (!postButton.isDisabled() && sendKeyPressed) postMessage();
postButton.setDisable(messageTextArea.getText().isBlank() && pendingAttachment == null || currentChat == null);
} else {
final var noMoreMessaging = "Go online to send messages";
if (!infoLabel.getText().equals(noMoreMessaging))
// Informing the user that he is a f*cking moron and should use Envoy online
// because he ran out of messageIDs to use
updateInfoLabel(noMoreMessaging, "infoLabel-error");
}
}
/**
* Actions to perform when the text was updated in the messageTextArea.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void messageTextUpdated() {
// Truncating messages that are too long and staying at the same position
if (messageTextArea.getText().length() >= MAX_MESSAGE_LENGTH) {
messageTextArea.setText(messageTextArea.getText().substring(0, MAX_MESSAGE_LENGTH));
messageTextArea.positionCaret(MAX_MESSAGE_LENGTH);
messageTextArea.setScrollTop(Double.MAX_VALUE);
}
updateRemainingCharsLabel();
}
/**
* Sets the text and text color of the {@code remainingChars} label.
*
* @since Envoy Client v0.1-beta
*/
private void updateRemainingCharsLabel() {
final int currentLength = messageTextArea.getText().length();
final int remainingLength = MAX_MESSAGE_LENGTH - currentLength;
remainingChars.setText(String.format("remaining chars: %d/%d", remainingLength, MAX_MESSAGE_LENGTH));
remainingChars.setTextFill(Color.rgb(currentLength, remainingLength, 0, 1));
}
/**
* Sends a new {@link Message} or {@link GroupMessage} to the server based on
* the text entered in the {@code messageTextArea} and the given attachment.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void postMessage() {
postingPermanentlyDisabled = !(client.isOnline() || localDB.getIDGenerator().hasNext());
if (postingPermanentlyDisabled) {
postButton.setDisable(true);
messageTextArea.setDisable(true);
messageTextArea.clear();
updateInfoLabel("You need to go online to send more messages", "infoLabel-error");
return;
}
final var text = messageTextArea.getText().strip();
try {
// Creating the message and its metadata
final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
.setText(text);
// Setting an attachment, if present
if (pendingAttachment != null) {
builder.setAttachment(pendingAttachment);
pendingAttachment = null;
updateAttachmentView(false);
}
// Building the final message
final var message = currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient())
: builder.build();
// Send message
writeProxy.writeMessage(message);
// Add message to LocalDB and update UI
currentChat.insert(message);
// Moving currentChat to the top
Platform.runLater(() -> {
chatList.getItems().remove(currentChat);
chatList.getItems().add(0, currentChat);
chatList.getSelectionModel().select(0);
localDB.getChats().remove(currentChat);
localDB.getChats().add(0, currentChat);
});
messageList.refresh();
scrollToMessageListEnd();
// Request a new ID generator if all IDs were used
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
messageTextArea.setText("");
postButton.setDisable(true);
updateRemainingCharsLabel();
}
/**
* Scrolls to the bottom of the {@code messageList}.
*
* @since Envoy Client v0.1-beta
*/
private void scrollToMessageListEnd() { messageList.scrollTo(messageList.getItems().size() - 1); }
/**
* Updates the {@code infoLabel}.
*
* @param text the text to use
* @param infoLabelID the id the the {@code infoLabel} should have so that it
* can be styled accordingly in CSS
* @since Envoy Client v0.1-beta
*/
private void updateInfoLabel(String text, String infoLabelID) {
infoLabel.setText(text);
infoLabel.setId(infoLabelID);
infoLabel.setVisible(true);
}
/**
* Updates the {@code attachmentView} in terms of visibility.<br>
* Additionally resets the shown image to
* {@code DEFAULT_ATTACHMENT_VIEW_IMAGE} if another image is currently
* present.
*
* @param visible whether the {@code attachmentView} should be displayed
* @since Envoy Client v0.1-beta
*/
private void updateAttachmentView(boolean visible) {
if (!attachmentView.getImage().equals(DEFAULT_ATTACHMENT_VIEW_IMAGE)) attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
attachmentView.setVisible(visible);
}
// Context menu actions
@FXML
private void deleteContact() { try {} catch (final NullPointerException e) {} }
@FXML
private void copyAndPostMessage() {
final var messageText = messageTextArea.getText();
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(messageText), null);
postMessage();
messageTextArea.setText(messageText);
updateRemainingCharsLabel();
postButton.setDisable(messageText.isBlank());
}
}

View File

@ -0,0 +1,130 @@
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(); }
}

View File

@ -0,0 +1,109 @@
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&auml;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(); }
}

View File

@ -0,0 +1,199 @@
package envoy.client.ui.controller;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import envoy.client.data.*;
import envoy.client.net.Client;
import envoy.client.net.WriteProxy;
import envoy.client.ui.ClearableTextField;
import envoy.client.ui.SceneContext;
import envoy.client.ui.Startup;
import envoy.data.LoginCredentials;
import envoy.data.User;
import envoy.data.User.UserStatus;
import envoy.event.EventBus;
import envoy.event.HandshakeRejection;
import envoy.exception.EnvoyException;
import envoy.util.Bounds;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>LoginDialog.java</strong><br>
* Created: <strong>03.04.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public final class LoginScene {
@FXML
private ClearableTextField userTextField;
@FXML
private PasswordField passwordField;
@FXML
private PasswordField repeatPasswordField;
@FXML
private Label repeatPasswordLabel;
@FXML
private CheckBox registerCheckBox;
@FXML
private Label connectionLabel;
private Client client;
private LocalDB localDB;
private CacheMap cacheMap;
private SceneContext sceneContext;
private static final Logger logger = EnvoyLog.getLogger(LoginScene.class);
private static final EventBus eventBus = EventBus.getInstance();
private static final ClientConfig config = ClientConfig.getInstance();
@FXML
private void initialize() {
connectionLabel.setText("Server: " + config.getServer() + ":" + config.getPort());
// Show an alert after an unsuccessful handshake
eventBus.register(HandshakeRejection.class, e -> Platform.runLater(() -> { new Alert(AlertType.ERROR, e.get()).showAndWait(); }));
}
/**
* 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
userTextField.requestFocus();
// Perform automatic login if configured
if (config.hasLoginCredentials()) performHandshake(config.getLoginCredentials());
}
@FXML
private void loginButtonPressed() {
// Prevent registration with unequal passwords
if (registerCheckBox.isSelected() && !passwordField.getText().equals(repeatPasswordField.getText())) {
new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait();
repeatPasswordField.clear();
} else if (!Bounds.isValidContactName(userTextField.getTextField().getText())) {
new Alert(AlertType.ERROR, "The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
userTextField.getTextField().clear();
} else performHandshake(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), registerCheckBox.isSelected(),
Startup.VERSION));
}
@FXML
private void offlineModeButtonPressed() {
attemptOfflineMode(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), false, Startup.VERSION));
}
@FXML
private void registerCheckboxChanged() {
// Make repeat password field and label visible / invisible
repeatPasswordField.setVisible(registerCheckBox.isSelected());
repeatPasswordLabel.setVisible(registerCheckBox.isSelected());
}
@FXML
private void abortLogin() {
logger.log(Level.INFO, "The login process has been cancelled. Exiting...");
System.exit(0);
}
private void performHandshake(LoginCredentials credentials) {
try {
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);
}
}

View File

@ -0,0 +1,59 @@
package envoy.client.ui.controller;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import envoy.client.ui.SceneContext;
import envoy.client.ui.settings.GeneralSettingsPane;
import envoy.client.ui.settings.SettingsPane;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>SettingsSceneController.java</strong><br>
* Created: <strong>10.04.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public class SettingsScene {
@FXML
private ListView<SettingsPane> settingsList;
@FXML
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
private void initialize() {
settingsList.setCellFactory(listView -> new ListCell<>() {
@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
private void settingsListClicked() {
final var pane = settingsList.getSelectionModel().getSelectedItem();
if (pane != null) {
titledPane.setText(pane.getTitle());
titledPane.setContent(pane);
}
}
@FXML
private void backButtonClicked() { sceneContext.pop(); }
}

View File

@ -0,0 +1,11 @@
/**
* 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
* @since Envoy Client v0.1-beta
*/
package envoy.client.ui.controller;