284 lines
9.5 KiB
Java
284 lines
9.5 KiB
Java
package client;
|
|
|
|
import javafx.animation.KeyFrame;
|
|
import javafx.animation.Timeline;
|
|
import javafx.application.Platform;
|
|
import javafx.fxml.FXML;
|
|
import javafx.scene.control.*;
|
|
import javafx.scene.layout.*;
|
|
import javafx.scene.text.Text;
|
|
import javafx.scene.text.TextFlow;
|
|
import javafx.stage.Stage;
|
|
import javafx.util.Duration;
|
|
import network.protocol.packets.ServerResponsePacket;
|
|
|
|
import java.net.InetSocketAddress;
|
|
import java.net.SocketException;
|
|
import java.time.Instant;
|
|
import java.time.ZoneId;
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.Optional;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
public class ClientGuiController implements ClientListener {
|
|
|
|
private Client client;
|
|
|
|
private boolean connected = true;
|
|
|
|
@FXML
|
|
private BorderPane vueContainer;
|
|
|
|
@FXML
|
|
private VBox roomList;
|
|
|
|
@FXML
|
|
private VBox chatList;
|
|
|
|
@FXML
|
|
private HBox chatInput;
|
|
|
|
@FXML
|
|
private ScrollPane chatPane;
|
|
|
|
@FXML
|
|
private Label roomName;
|
|
|
|
public void setClient(Client client) {
|
|
this.client = client;
|
|
}
|
|
|
|
@FXML
|
|
public void initialize() throws SocketException {
|
|
client = new Client(new InetSocketAddress("192.168.163.131", 6665), this, UsernameSingleton.getInstance().getUsername());
|
|
Platform.runLater(() -> {
|
|
Stage stage = (Stage) vueContainer.getScene().getWindow();
|
|
stage.setResizable(true); // Fixed a bug that made the close button disappear under my Wayland setup (don't know if it's the same for x11)
|
|
stage.setOnCloseRequest(event -> {
|
|
connected = false;
|
|
client.close();
|
|
});
|
|
chatList.heightProperty().addListener((obs, oldVal, newVal) -> chatPane.setVvalue(1.0));
|
|
});
|
|
|
|
requestRoomsRegularly();
|
|
}
|
|
|
|
/**
|
|
* Request the list of rooms from the server every second.
|
|
*/
|
|
private void requestRoomsRegularly() {
|
|
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(10), event -> client.RequestRoomList()));
|
|
timeline.setCycleCount(Timeline.INDEFINITE);
|
|
timeline.play();
|
|
}
|
|
|
|
@Override
|
|
public void handleDisconnect() {
|
|
System.out.println("Disconnected");
|
|
}
|
|
|
|
/**
|
|
* Create an alert dialog when the connection to the server fails.
|
|
*/
|
|
@Override
|
|
public void handleConnexionError() {
|
|
if(connected) {
|
|
Platform.runLater(() -> {
|
|
Alert alert = new Alert(Alert.AlertType.ERROR);
|
|
alert.setTitle("Connection");
|
|
alert.setHeaderText("Connection failed");
|
|
Optional<ButtonType> res = alert.showAndWait();
|
|
if (res.isPresent() && res.get() == ButtonType.OK) {
|
|
((Stage) vueContainer.getScene().getWindow()).close();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create an alert dialog when the connection to the server is successful.
|
|
*/
|
|
@Override
|
|
public void handleConnect() {
|
|
Platform.runLater(() -> {
|
|
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
|
alert.setTitle("Connection");
|
|
alert.setHeaderText("Connection to the server successful");
|
|
alert.showAndWait();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Format and insert the received chat message into the chat list.
|
|
* @param time the time the message was sent
|
|
* @param chatter the name of the person who sent the message
|
|
* @param content the content of the message
|
|
*/
|
|
@Override
|
|
public void handleChatMessage(Instant time, String chatter, String content) {
|
|
String untagged = untag(content);
|
|
String messageColor = Objects.equals(content, untagged) ? "lightblue" : "lightgreen";
|
|
Platform.runLater(() -> {
|
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm")
|
|
.withZone(ZoneId.systemDefault());
|
|
String timeString = formatter.format(time);
|
|
Text chatterText = new Text(chatter + ": ");
|
|
chatterText.setStyle("-fx-fill: black; -fx-font-weight: bold;");
|
|
TextFlow messageText = formatMessage(untagged);
|
|
TextFlow wholeMessage = new TextFlow(chatterText, messageText);
|
|
Text timeText = new Text(" " + timeString);
|
|
timeText.setStyle("-fx-fill: gray; -fx-font-size: 10px;");
|
|
HBox messageContainer = new HBox(5, wholeMessage, timeText);
|
|
messageContainer.setStyle("-fx-background-color: " + messageColor + "; -fx-padding: 8; -fx-background-radius: 5;");
|
|
messageContainer.setMaxWidth(Double.MAX_VALUE);
|
|
HBox.setHgrow(messageText, Priority.ALWAYS);
|
|
chatList.getChildren().add(messageContainer);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Remove the tag from the message (handle ANSI characters encoded by the server)
|
|
* @param message the message to untag
|
|
* @return the untagged message
|
|
*/
|
|
public String untag(String message) {
|
|
Pattern pattern = Pattern.compile("\u001B\\[44;30m(.*?)\u001B\\[49;39m");
|
|
Matcher matcher = pattern.matcher(message);
|
|
|
|
StringBuilder result = new StringBuilder();
|
|
while (matcher.find()) {
|
|
String taggedName = matcher.group(1);
|
|
matcher.appendReplacement(result, taggedName);
|
|
}
|
|
matcher.appendTail(result);
|
|
|
|
return result.toString();
|
|
}
|
|
|
|
/**
|
|
* Display the list of rooms in the room list.
|
|
* @param roomNames the list of room names
|
|
*/
|
|
@Override
|
|
public void handleRoomList(List<String> roomNames) {
|
|
Platform.runLater(() -> {
|
|
roomList.getChildren().clear();
|
|
for (String roomName : roomNames) {
|
|
Button button = new Button(roomName);
|
|
button.setOnAction(event -> {
|
|
client.SendJoinRoom(roomName);
|
|
createChatEnv(roomName);
|
|
});
|
|
roomList.getChildren().add(button);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create the chat (message wall + input) environment.
|
|
* @param roomName the name of the room
|
|
*/
|
|
private void createChatEnv(String roomName) {
|
|
Platform.runLater(() -> {
|
|
this.roomName.setText("Room: " + roomName);
|
|
chatList.getChildren().clear();
|
|
chatInput.getChildren().clear();
|
|
Button leaveButton = new Button("Leave room");
|
|
leaveButton.setOnAction(event -> {
|
|
client.SendLeaveRoom();
|
|
this.roomName.setText("");
|
|
chatList.getChildren().clear();
|
|
chatInput.getChildren().clear();
|
|
});
|
|
chatList.getChildren().add(leaveButton);
|
|
|
|
TextField messageInput = new TextField();
|
|
messageInput.setPromptText("Type a message...");
|
|
messageInput.getStyleClass().add("message-input");
|
|
HBox.setHgrow(messageInput, Priority.ALWAYS);
|
|
messageInput.onActionProperty().set(event -> {
|
|
client.SendChatMessage(messageInput.getText());
|
|
messageInput.clear();
|
|
});
|
|
chatInput.getChildren().add(messageInput);
|
|
|
|
Button sendButton = new Button("Send");
|
|
sendButton.getStyleClass().add("send-button");
|
|
sendButton.setOnAction(event -> {
|
|
client.SendChatMessage(messageInput.getText());
|
|
messageInput.clear();
|
|
});
|
|
chatInput.getChildren().add(sendButton);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Colorize the message according to the color codes.
|
|
* @param content the content of the message
|
|
* @return the formatted message
|
|
*/
|
|
private TextFlow formatMessage(String content) {
|
|
TextFlow textFlow = new TextFlow();
|
|
// Evil regex : match every sequence starting with a color code ending with the next color code
|
|
Pattern pattern = Pattern.compile("&([rbgyn])([^&]*)");
|
|
Matcher matcher = pattern.matcher(content);
|
|
int lastIndex = 0;
|
|
|
|
while (matcher.find()) {
|
|
if (matcher.start() > lastIndex) {
|
|
textFlow.getChildren().add(new Text(content.substring(lastIndex, matcher.start())));
|
|
}
|
|
|
|
String colorCode = matcher.group(1);
|
|
String textPart = matcher.group(2);
|
|
|
|
String color = switch (colorCode) {
|
|
case "r" -> "red";
|
|
case "b" -> "blue";
|
|
case "g" -> "green";
|
|
case "y" -> "gray";
|
|
default -> "black";
|
|
};
|
|
|
|
Text coloredText = new Text(textPart);
|
|
coloredText.setStyle("-fx-fill: " + color + ";");
|
|
textFlow.getChildren().add(coloredText);
|
|
|
|
lastIndex = matcher.end();
|
|
}
|
|
|
|
// Add the remaining text (works if there's no color code at all)
|
|
if (lastIndex < content.length()) {
|
|
textFlow.getChildren().add(new Text(content.substring(lastIndex)));
|
|
}
|
|
|
|
return textFlow;
|
|
}
|
|
|
|
@Override
|
|
public void handleServerResponse(ServerResponsePacket.Response response) {
|
|
|
|
}
|
|
|
|
@Override
|
|
public void handleActualRoom(String roomName) {
|
|
|
|
}
|
|
|
|
public void createRoom() {
|
|
TextInputDialog dialog = new TextInputDialog();
|
|
dialog.setTitle("Create a room");
|
|
dialog.setHeaderText("Enter the name of the room");
|
|
dialog.setContentText("Room name:");
|
|
Optional<String> result = dialog.showAndWait();
|
|
result.ifPresent(name -> {
|
|
client.SendCreateRoom(name);
|
|
createChatEnv(name);
|
|
});
|
|
}
|
|
}
|