Compare commits
10
Commits
main
...
e5e16c0611
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5e16c0611 | ||
|
|
abb0819cc0 | ||
|
|
eb88aaeed0 | ||
|
|
60fe662629 | ||
|
|
95d46dd02a | ||
|
|
0c3b43cbd7 | ||
|
|
47b2c3f85f | ||
|
|
bda8e144ff | ||
|
|
8454be1cec | ||
|
|
079145b4e2 |
@@ -13,5 +13,31 @@
|
|||||||
<maven.compiler.target>25</maven.compiler.target>
|
<maven.compiler.target>25</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-controls</artifactId>
|
||||||
|
<version>21</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-fxml</artifactId>
|
||||||
|
<version>21</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
|
<artifactId>exec-maven-plugin</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
|
<configuration>
|
||||||
|
<!-- آدرس کلاس اصلی کلاینت را اینجا بنویس -->
|
||||||
|
<mainClass>com.university.chat.Client.UI.Launcher</mainClass>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.university.chat.Client.Net;
|
||||||
|
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class ServerListener implements Runnable {
|
||||||
|
|
||||||
|
private final ObjectInputStream in;
|
||||||
|
private Consumer<Object> onMessageReceived;
|
||||||
|
private final Consumer<Exception> onConnectionError;
|
||||||
|
|
||||||
|
private volatile boolean running = true;
|
||||||
|
|
||||||
|
public ServerListener(ObjectInputStream in,
|
||||||
|
Consumer<Object> onMessageReceived,
|
||||||
|
Consumer<Exception> onConnectionError) {
|
||||||
|
this.in = in;
|
||||||
|
this.onMessageReceived = onMessageReceived;
|
||||||
|
this.onConnectionError = onConnectionError;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (running) {
|
||||||
|
Object object = in.readObject();
|
||||||
|
if (onMessageReceived != null) {
|
||||||
|
onMessageReceived.accept(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (running && onConnectionError != null) {
|
||||||
|
onConnectionError.accept(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnMessageReceived(Consumer<Object> onMessageReceived) {
|
||||||
|
this.onMessageReceived = onMessageReceived;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package com.university.chat.Client.Net;
|
||||||
|
|
||||||
|
import com.university.chat.Client.UI.ChatController;
|
||||||
|
import com.university.chat.Common.ChatMessage;
|
||||||
|
import com.university.chat.Common.FileMessage;
|
||||||
|
import com.university.chat.Common.MessageType;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class chatClient {
|
||||||
|
|
||||||
|
private Socket socket;
|
||||||
|
private ObjectOutputStream out;
|
||||||
|
private ObjectInputStream in;
|
||||||
|
|
||||||
|
private Thread listenerThread;
|
||||||
|
private ServerListener serverListener;
|
||||||
|
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
public void connect(String host, int port) throws IOException {
|
||||||
|
|
||||||
|
socket = new Socket(host, port);
|
||||||
|
|
||||||
|
out = new ObjectOutputStream(socket.getOutputStream());
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
in = new ObjectInputStream(socket.getInputStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startListening(Consumer<Object> onMessageReceived,
|
||||||
|
Consumer<Exception> onConnectionError) {
|
||||||
|
|
||||||
|
serverListener = new ServerListener(in, onMessageReceived, onConnectionError);
|
||||||
|
|
||||||
|
listenerThread = new Thread(serverListener);
|
||||||
|
listenerThread.setDaemon(true);
|
||||||
|
listenerThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void login(String username) throws IOException {
|
||||||
|
this.username = username;
|
||||||
|
|
||||||
|
ChatMessage loginMessage = new ChatMessage(
|
||||||
|
MessageType.LOGIN,
|
||||||
|
username,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
sendObject(loginMessage);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendPublicMessage(String content) throws IOException {
|
||||||
|
ChatMessage message = new ChatMessage(
|
||||||
|
MessageType.PUBLIC_MESSAGE,
|
||||||
|
username,
|
||||||
|
null,
|
||||||
|
content
|
||||||
|
);
|
||||||
|
|
||||||
|
sendObject(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendPrivateMessage(String receiver, String content) throws IOException {
|
||||||
|
ChatMessage message = new ChatMessage(
|
||||||
|
MessageType.PRIVATE_MESSAGE,
|
||||||
|
username,
|
||||||
|
receiver,
|
||||||
|
content
|
||||||
|
);
|
||||||
|
|
||||||
|
sendObject(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requestUserList() throws IOException {
|
||||||
|
ChatMessage message = new ChatMessage(
|
||||||
|
MessageType.USER_LIST,
|
||||||
|
username,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
sendObject(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void sendFile(String receiver, File file) throws IOException {
|
||||||
|
if (file == null || !file.exists() || !file.isFile()) {
|
||||||
|
throw new FileNotFoundException("Invalid file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] fileData = Files.readAllBytes(file.toPath());
|
||||||
|
|
||||||
|
|
||||||
|
FileMessage fileMessage = new FileMessage(
|
||||||
|
username,
|
||||||
|
receiver,
|
||||||
|
file.getName(),
|
||||||
|
fileData
|
||||||
|
);
|
||||||
|
|
||||||
|
sendObject(fileMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void sendObject(Object object) throws IOException {
|
||||||
|
if (out == null) {
|
||||||
|
throw new IOException("Client is not connected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
out.writeObject(object);
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConnected() {
|
||||||
|
return socket != null && socket.isConnected() && !socket.isClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
if (serverListener != null) {
|
||||||
|
serverListener.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listenerThread != null) {
|
||||||
|
listenerThread.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in != null) {
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out != null) {
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socket != null) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("Error while closing client: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerListener getServerListener() {
|
||||||
|
return serverListener;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.university.chat.Client;
|
|
||||||
|
|
||||||
public class ServerListener implements Runnable{
|
|
||||||
// TODO: store the ObjectInputStream from the user socket
|
|
||||||
// (this should be the same input stream the
|
|
||||||
// chatClient created when connecting)
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
// TODO: In an infinite loop read objects from the server
|
|
||||||
// - if it's a ChatMessage -> print "<sender>: <content>"
|
|
||||||
// - if it's a FileMessage -> print that a file was received
|
|
||||||
// (filename + sender), it's already
|
|
||||||
// saved to disk by the server.
|
|
||||||
} catch (Exception e){
|
|
||||||
System.out.println("Disconnected from server");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.university.chat.Client.UI;
|
||||||
|
|
||||||
|
|
||||||
|
import javafx.application.Application;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.StageStyle;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class ChatApp extends Application {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(Stage stage) throws IOException {
|
||||||
|
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("login.fxml"));
|
||||||
|
Scene scene = new Scene(fxmlLoader.load());
|
||||||
|
scene.getStylesheets().add(
|
||||||
|
getClass().getResource("/chat_style.css").toExternalForm()
|
||||||
|
);
|
||||||
|
|
||||||
|
stage.setTitle("Login");
|
||||||
|
stage.initStyle(StageStyle.UNDECORATED);
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
package com.university.chat.Client.UI;
|
||||||
|
|
||||||
|
import com.university.chat.Client.Net.chatClient;
|
||||||
|
import com.university.chat.Client.TransferProgress;
|
||||||
|
import com.university.chat.Common.ChatMessage;
|
||||||
|
import com.university.chat.Common.FileMessage;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
import javafx.stage.FileChooser;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class ChatController {
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ListView<String> usersList;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TextArea publicChatArea;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private AnchorPane chatAnchor;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TextField messageField;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private BorderPane titleBar;
|
||||||
|
|
||||||
|
private double xOffset = 0;
|
||||||
|
private double yOffset = 0;
|
||||||
|
|
||||||
|
private chatClient client;
|
||||||
|
private String selectedUser = null;
|
||||||
|
|
||||||
|
private final Map<String, BorderPane> privateChatViews = new HashMap<>();
|
||||||
|
private final Map<String, TextArea> privateTextAreas = new HashMap<>();
|
||||||
|
private final Map<String, TextField> privateInputFields = new HashMap<>();
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private VBox fileTransferBox;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label fileTransferLabel;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ProgressBar fileTransferProgress;
|
||||||
|
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void initialize() {
|
||||||
|
|
||||||
|
titleBar.setOnMousePressed(event -> {
|
||||||
|
xOffset = event.getSceneX();
|
||||||
|
yOffset = event.getSceneY();
|
||||||
|
});
|
||||||
|
|
||||||
|
titleBar.setOnMouseDragged(event -> {
|
||||||
|
Stage stage = (Stage) titleBar.getScene().getWindow();
|
||||||
|
stage.setX(event.getScreenX() - xOffset);
|
||||||
|
stage.setY(event.getScreenY() - yOffset);
|
||||||
|
});
|
||||||
|
|
||||||
|
usersList.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> {
|
||||||
|
|
||||||
|
if (newVal == null || newVal.isBlank()) {
|
||||||
|
selectedUser = null;
|
||||||
|
chatAnchor.getChildren().clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String clickedUser = newVal.trim();
|
||||||
|
|
||||||
|
if (client != null && clickedUser.equals(client.getUsername())) {
|
||||||
|
selectedUser = null;
|
||||||
|
chatAnchor.getChildren().clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedUser = clickedUser;
|
||||||
|
openPrivateChat(clickedUser);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClient(chatClient client) {
|
||||||
|
|
||||||
|
this.client = client;
|
||||||
|
|
||||||
|
if (client.getServerListener() != null) {
|
||||||
|
client.getServerListener().setOnMessageReceived(this::handleIncomingMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.requestUserList();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleIncomingMessage(Object obj) {
|
||||||
|
|
||||||
|
if (obj instanceof ChatMessage msg) {
|
||||||
|
|
||||||
|
switch (msg.getType()) {
|
||||||
|
|
||||||
|
case USER_LIST -> {
|
||||||
|
String content = msg.getContent();
|
||||||
|
if (content != null) {
|
||||||
|
|
||||||
|
List<String> users = Arrays.stream(content.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(u -> !u.isEmpty())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
updateUsersList(users);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case PUBLIC_MESSAGE -> Platform.runLater(() ->
|
||||||
|
publicChatArea.appendText(msg.getSender() + ": " + msg.getContent() + "\n"));
|
||||||
|
|
||||||
|
case PRIVATE_MESSAGE -> Platform.runLater(() ->
|
||||||
|
appendPrivateMessage(msg.getSender(),
|
||||||
|
msg.getSender() + ": " + msg.getContent()));
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (obj instanceof FileMessage msg) {
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
|
||||||
|
FileChooser chooser = new FileChooser();
|
||||||
|
chooser.setInitialFileName(msg.getFilename());
|
||||||
|
|
||||||
|
File file = chooser.showSaveDialog(usersList.getScene().getWindow());
|
||||||
|
|
||||||
|
if(file != null){
|
||||||
|
try {
|
||||||
|
Files.write(file.toPath(), msg.getData());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateUsersList(List<String> users) {
|
||||||
|
Platform.runLater(() ->
|
||||||
|
usersList.getItems().setAll(users));
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void sendPublicMessage() {
|
||||||
|
|
||||||
|
String text = messageField.getText();
|
||||||
|
|
||||||
|
if (text == null || text.trim().isEmpty()) return;
|
||||||
|
|
||||||
|
text = text.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
client.sendPublicMessage(text);
|
||||||
|
publicChatArea.appendText("Me: " + text + "\n");
|
||||||
|
messageField.clear();
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void sendPrivateMessage(String receiver) {
|
||||||
|
|
||||||
|
TextField inputField = privateInputFields.get(receiver);
|
||||||
|
TextArea chatArea = privateTextAreas.get(receiver);
|
||||||
|
|
||||||
|
if (inputField == null || chatArea == null) return;
|
||||||
|
|
||||||
|
String text = inputField.getText();
|
||||||
|
|
||||||
|
if (text == null || text.trim().isEmpty()) return;
|
||||||
|
|
||||||
|
text = text.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
client.sendPrivateMessage(receiver, text);
|
||||||
|
chatArea.appendText("Me: " + text + "\n");
|
||||||
|
inputField.clear();
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void sendFile() {
|
||||||
|
|
||||||
|
FileChooser fileChooser = new FileChooser();
|
||||||
|
fileChooser.setTitle("Select file to send");
|
||||||
|
|
||||||
|
Stage stage = (Stage) usersList.getScene().getWindow();
|
||||||
|
File file = fileChooser.showOpenDialog(stage);
|
||||||
|
|
||||||
|
if (file == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
updateFileTransfer(file.length());
|
||||||
|
client.sendFile(selectedUser,file);
|
||||||
|
publicChatArea.appendText("Me sent file: " + file.getName() + "\n");
|
||||||
|
}
|
||||||
|
catch (Exception e){
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void openPrivateChat(String user) {
|
||||||
|
|
||||||
|
BorderPane chatView = privateChatViews.get(user);
|
||||||
|
|
||||||
|
if (chatView == null) {
|
||||||
|
chatView = createPrivateChatView(user);
|
||||||
|
privateChatViews.put(user, chatView);
|
||||||
|
}
|
||||||
|
|
||||||
|
chatAnchor.getChildren().setAll(chatView);
|
||||||
|
|
||||||
|
AnchorPane.setTopAnchor(chatView, 0.0);
|
||||||
|
AnchorPane.setBottomAnchor(chatView, 0.0);
|
||||||
|
AnchorPane.setLeftAnchor(chatView, 0.0);
|
||||||
|
AnchorPane.setRightAnchor(chatView, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BorderPane createPrivateChatView(String user) {
|
||||||
|
|
||||||
|
TextArea chatArea = new TextArea();
|
||||||
|
chatArea.setEditable(false);
|
||||||
|
chatArea.setWrapText(true);
|
||||||
|
|
||||||
|
TextField inputField = new TextField();
|
||||||
|
inputField.setPromptText("Message to " + user);
|
||||||
|
|
||||||
|
Button sendButton = new Button("Send");
|
||||||
|
|
||||||
|
sendButton.setOnAction(e -> sendPrivateMessage(user));
|
||||||
|
inputField.setOnAction(e -> sendPrivateMessage(user));
|
||||||
|
|
||||||
|
HBox bottom = new HBox(8, inputField, sendButton);
|
||||||
|
bottom.setPadding(new Insets(8));
|
||||||
|
inputField.setPrefWidth(300);
|
||||||
|
|
||||||
|
BorderPane root = new BorderPane();
|
||||||
|
root.setCenter(chatArea);
|
||||||
|
root.setBottom(bottom);
|
||||||
|
|
||||||
|
privateTextAreas.put(user, chatArea);
|
||||||
|
privateInputFields.put(user, inputField);
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void appendPrivateMessage(String user, String line) {
|
||||||
|
|
||||||
|
BorderPane view = privateChatViews.get(user);
|
||||||
|
|
||||||
|
if (view == null) {
|
||||||
|
view = createPrivateChatView(user);
|
||||||
|
privateChatViews.put(user, view);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextArea area = privateTextAreas.get(user);
|
||||||
|
|
||||||
|
if (area != null) {
|
||||||
|
area.appendText(line + "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void showFileTransfer(String fileName) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
fileTransferBox.setVisible(true);
|
||||||
|
fileTransferBox.setManaged(true);
|
||||||
|
fileTransferProgress.setProgress(0);
|
||||||
|
fileTransferLabel.setText("Sending " + fileName + "...");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateFileTransfer(long length) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
|
||||||
|
fileTransferProgress.setProgress(length);
|
||||||
|
fileTransferLabel.setText(String.format(
|
||||||
|
"Sending... %d",
|
||||||
|
(int) (length * 100)
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void finishFileTransfer(String fileName) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
fileTransferProgress.setProgress(1.0);
|
||||||
|
fileTransferLabel.setText(fileName + " sent successfully");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void closeApp() {
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void minimize() {
|
||||||
|
Stage stage = (Stage) usersList.getScene().getWindow();
|
||||||
|
stage.setIconified(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void maximize() {
|
||||||
|
Stage stage = (Stage) usersList.getScene().getWindow();
|
||||||
|
stage.setMaximized(!stage.isMaximized());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.university.chat.Client.UI;
|
||||||
|
import javafx.application.Application;
|
||||||
|
|
||||||
|
public class Launcher{
|
||||||
|
public static void main(String[] args) {
|
||||||
|
|
||||||
|
Application.launch(ChatApp.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package com.university.chat.Client.UI;
|
||||||
|
|
||||||
|
import com.university.chat.Client.Net.chatClient;
|
||||||
|
import com.university.chat.Common.ChatMessage;
|
||||||
|
import com.university.chat.Common.MessageType;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Parent;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
public class LoginController {
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public HBox titleBar;
|
||||||
|
|
||||||
|
private double xOffset = 0;
|
||||||
|
private double yOffset = 0;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TextField usernameField;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button nameButton;
|
||||||
|
|
||||||
|
private final chatClient client = new chatClient();
|
||||||
|
|
||||||
|
private ChatController chatController;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
titleBar.setOnMousePressed(event -> {
|
||||||
|
xOffset = event.getSceneX();
|
||||||
|
yOffset = event.getSceneY();
|
||||||
|
});
|
||||||
|
|
||||||
|
titleBar.setOnMouseDragged(event -> {
|
||||||
|
Stage stage = (Stage) titleBar.getScene().getWindow();
|
||||||
|
stage.setX(event.getScreenX() - xOffset);
|
||||||
|
stage.setY(event.getScreenY() - yOffset);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void onConnectClicked() {
|
||||||
|
String username = usernameField.getText().trim();
|
||||||
|
|
||||||
|
if (username.isEmpty()) {
|
||||||
|
showError("Username cannot be empty.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nameButton.setDisable(true);
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
|
||||||
|
client.connect("localhost", 9000);
|
||||||
|
|
||||||
|
client.startListening(
|
||||||
|
this::handleServerMessage,
|
||||||
|
this::handleConnectionError
|
||||||
|
);
|
||||||
|
|
||||||
|
client.login(username);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
showError("Failed to connect to server.\nMake sure the server is running.\n\n" + e.getMessage());
|
||||||
|
nameButton.setDisable(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void handleServerMessage(Object obj) {
|
||||||
|
if (obj instanceof ChatMessage msg) {
|
||||||
|
|
||||||
|
if (msg.getType() == MessageType.LOGIN_SUCCESS) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
showInfo("Login successful!");
|
||||||
|
openChatWindow();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (msg.getType() == MessageType.LOGIN_FAILED) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
showError("Login failed: username taken");
|
||||||
|
nameButton.setDisable(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (msg.getType() == MessageType.USER_LIST) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if (chatController != null) {
|
||||||
|
String content = msg.getContent();
|
||||||
|
java.util.List<String> users = java.util.Arrays.asList(content.split(","));
|
||||||
|
chatController.updateUsersList(users);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if (chatController != null) {
|
||||||
|
chatController.sendPublicMessage();
|
||||||
|
}
|
||||||
|
System.out.println("Server: " + msg.getContent());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleConnectionError(Exception e) {
|
||||||
|
Platform.runLater(() -> showError("Connection lost: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void showError(String text) {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||||
|
alert.setTitle("Error");
|
||||||
|
alert.setHeaderText(null);
|
||||||
|
alert.setContentText(text);
|
||||||
|
alert.showAndWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showInfo(String text) {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||||
|
alert.setTitle("Info");
|
||||||
|
alert.setHeaderText(null);
|
||||||
|
alert.setContentText(text);
|
||||||
|
alert.showAndWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openChatWindow() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
FXMLLoader loader = new FXMLLoader(
|
||||||
|
getClass().getResource("chat.fxml")
|
||||||
|
);
|
||||||
|
|
||||||
|
Parent root = loader.load();
|
||||||
|
|
||||||
|
this.chatController = loader.getController();
|
||||||
|
this.chatController.setClient(client);
|
||||||
|
|
||||||
|
Stage stage = (Stage) usernameField.getScene().getWindow();
|
||||||
|
|
||||||
|
stage.setScene(new Scene(root, 900, 600));
|
||||||
|
|
||||||
|
stage.getScene().getStylesheets().add(
|
||||||
|
getClass().getResource("/chat_style.css").toExternalForm()
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void closeApp() {
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void minimize() {
|
||||||
|
Stage stage = (Stage) usernameField.getScene().getWindow();
|
||||||
|
stage.setIconified(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void maximize() {
|
||||||
|
Stage stage = (Stage) usernameField.getScene().getWindow();
|
||||||
|
stage.setMaximized(!stage.isMaximized());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package com.university.chat.Client;
|
|
||||||
|
|
||||||
public class chatClient {
|
|
||||||
public static void main() {
|
|
||||||
// TODO: Connecting to the server
|
|
||||||
// 1. Create a socket and connect to the server
|
|
||||||
// 2. Create an ObjectOutputStream (out) and ObjectInputStream (in)
|
|
||||||
// from the socket's streams — output FIRST, then input.
|
|
||||||
// 2. Get the username, and send a LOGIN ChatMessage with that username
|
|
||||||
// 3. Start a new Thread running a ServerListener(in) so incoming
|
|
||||||
// messages are handled concurrently.
|
|
||||||
|
|
||||||
while (true){
|
|
||||||
try {
|
|
||||||
// TODO: Program loop — read a line from the console and act on it:
|
|
||||||
// - "/msg <user> <text>" -> build & send a PRIVATE_MESSAGE
|
|
||||||
// - "/users" -> build & send a USER_LIST request
|
|
||||||
// - "/sendfile <user> <path>" -> read the file into a byte[]
|
|
||||||
// (you can use TransferProgress
|
|
||||||
// to show progress)
|
|
||||||
// and send it as a FileMessage
|
|
||||||
// - anything else -> send a PUBLIC_MESSAGE
|
|
||||||
// Remember to flush() the output stream after writeObject().
|
|
||||||
} catch (Exception e){
|
|
||||||
System.out.println("command failed: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,37 @@
|
|||||||
package com.university.chat.Server;
|
package com.university.chat.Server;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
public class ChatServer {
|
public class ChatServer {
|
||||||
// TODO: declare a single shared UserManager instance (static final)
|
|
||||||
// This MUST be shared by all ClientSession threads so that
|
private static final UserManager userManager = new UserManager();
|
||||||
// broadcasting and private messaging work correctly.
|
private static ServerSocket serverSocket;
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
// TODO: Create a ServerSocket
|
|
||||||
|
|
||||||
// TODO: In an infinite loop:
|
try {
|
||||||
// accept an incoming client connection
|
serverSocket = new ServerSocket(9000);
|
||||||
// make a new thread running ClientSession for each user.
|
System.out.printf("server started. port: %s\n" ,9000);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
|
||||||
|
Socket s = serverSocket.accept();
|
||||||
|
System.out.println("new client accepted.");
|
||||||
|
ClientSession clientSession = new ClientSession(s, userManager);
|
||||||
|
Thread thread = new Thread(clientSession);
|
||||||
|
thread.start();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
|
||||||
|
System.err.println("Error running thread.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,62 +2,145 @@ package com.university.chat.Server;
|
|||||||
|
|
||||||
import com.university.chat.Common.ChatMessage;
|
import com.university.chat.Common.ChatMessage;
|
||||||
import com.university.chat.Common.FileMessage;
|
import com.university.chat.Common.FileMessage;
|
||||||
|
import com.university.chat.Common.MessageType;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
|
||||||
public class ClientSession implements Runnable {
|
public class ClientSession implements Runnable {
|
||||||
|
|
||||||
private String username;
|
private String username;
|
||||||
|
private Socket socket;
|
||||||
|
private final UserManager userManager;
|
||||||
|
private ObjectOutputStream out;
|
||||||
|
private ObjectInputStream in;
|
||||||
|
|
||||||
public ClientSession(Socket socket, UserManager userManager) {
|
public ClientSession(Socket socket, UserManager userManager) {
|
||||||
// TODO : Create an ObjectOutputStream from socket.getOutputStream()
|
|
||||||
// and an ObjectInputStream from socket.getInputStream().
|
this.userManager = userManager;
|
||||||
|
this.socket = socket;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.out = new ObjectOutputStream(socket.getOutputStream());
|
||||||
|
out.flush();
|
||||||
|
this.in = new ObjectInputStream(socket.getInputStream());
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
System.out.println("Error initializing stream: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
|
System.out.println("welcome");
|
||||||
|
|
||||||
// TODO: Welcome the user (login step)
|
Object object = in.readObject();
|
||||||
// 1. Read the first object sent by the client.
|
|
||||||
// 2. Check it's a ChatMessage with type LOGIN.
|
|
||||||
// 3. Extract the username.
|
|
||||||
// 4. Try to register the user via userManager.addUser(...).
|
|
||||||
// 5. If the username is taken, send back LOGIN_FAILED and close the socket.
|
|
||||||
// 6. Otherwise, create the user's folders with FileManager.createUserFolders(...)
|
|
||||||
// and send back LOGIN_SUCCESS.
|
|
||||||
|
|
||||||
// TODO: Main message loop
|
if (object instanceof ChatMessage login && login.getType() == MessageType.LOGIN) {
|
||||||
// In a loop, call in.readObject(), you can separate messages by their type:
|
|
||||||
// - if it's a ChatMessage -> call handleChatMessage(msg)
|
|
||||||
// - if it's a FileMessage -> call handleFileMessage(fileMsg)
|
|
||||||
// Keep looping until the connection is closed (an exception will be thrown).
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
username = login.getSender();
|
||||||
|
|
||||||
|
if (userManager.addUser(username, this)) {
|
||||||
|
System.out.printf("Successfully registered: %s\n", username);
|
||||||
|
|
||||||
|
FileManager.createUserFolders(username);
|
||||||
|
|
||||||
|
out.writeObject(
|
||||||
|
new ChatMessage(MessageType.LOGIN_SUCCESS,
|
||||||
|
"[server]",
|
||||||
|
username,
|
||||||
|
null
|
||||||
|
));
|
||||||
|
|
||||||
|
out.flush();
|
||||||
|
broadcastUserList();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
out.writeObject(
|
||||||
|
new ChatMessage(MessageType.LOGIN_FAILED,
|
||||||
|
"[server]",
|
||||||
|
username,
|
||||||
|
null
|
||||||
|
));
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
socket.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
socket.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
|
||||||
|
Object msg = in.readObject();
|
||||||
|
|
||||||
|
if (msg instanceof ChatMessage chatMsg) {
|
||||||
|
handleChatMessage(chatMsg);
|
||||||
|
}
|
||||||
|
else if (msg instanceof FileMessage fileMsg) {
|
||||||
|
handleFileMessage(fileMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException | ClassNotFoundException e) {
|
||||||
System.out.println("Disconnected: " + username);
|
System.out.println("Disconnected: " + username);
|
||||||
} finally {
|
}
|
||||||
// TODO: Remove the user from UserManager so they no longer
|
finally {
|
||||||
// receive broadcasts or appear in users list
|
if (username != null) {
|
||||||
|
userManager.removeUser(username);
|
||||||
|
broadcastUserList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void handleChatMessage(ChatMessage msg) throws IOException {
|
private void handleChatMessage(ChatMessage msg) throws IOException {
|
||||||
switch (msg.getType()) {
|
switch (msg.getType()) {
|
||||||
case PUBLIC_MESSAGE -> {
|
case PUBLIC_MESSAGE -> {
|
||||||
// TODO: Broadcast this message to every connected client.
|
Iterable<ClientSession> users = userManager.getAllSessions();
|
||||||
|
for (ClientSession user : users) {
|
||||||
|
if (user != this) {
|
||||||
|
user.sendObject(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case PRIVATE_MESSAGE -> {
|
case PRIVATE_MESSAGE -> {
|
||||||
// TODO: Forward this message to the receiver user.
|
|
||||||
|
ClientSession receiver = userManager.getUser(msg.getReceiver());
|
||||||
|
if (receiver != null) {
|
||||||
|
receiver.sendObject(msg);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sendObject(new ChatMessage(
|
||||||
|
MessageType.PRIVATE_MESSAGE,
|
||||||
|
"[server]",
|
||||||
|
username,
|
||||||
|
"User not found: " + msg.getReceiver()
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case USER_LIST -> {
|
case USER_LIST -> {
|
||||||
// TODO: Reply to the requester with the list of online users.
|
|
||||||
|
sendObject(new ChatMessage(MessageType.USER_LIST,
|
||||||
|
"[server]",
|
||||||
|
username,
|
||||||
|
userManager.listUsers())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private synchronized void sendObject(Object obj) throws IOException {
|
||||||
|
out.writeObject(obj);
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
private void handleFileMessage(FileMessage fileMsg) throws IOException {
|
private void handleFileMessage(FileMessage fileMsg) throws IOException {
|
||||||
// Storing the file
|
// Storing the file
|
||||||
var sentPath = FileManager.getSentPath(fileMsg.getSender(), fileMsg.getFilename());
|
var sentPath = FileManager.getSentPath(fileMsg.getSender(), fileMsg.getFilename());
|
||||||
@@ -66,6 +149,26 @@ public class ClientSession implements Runnable {
|
|||||||
Files.write(sentPath, fileMsg.getData());
|
Files.write(sentPath, fileMsg.getData());
|
||||||
Files.write(recvPath, fileMsg.getData());
|
Files.write(recvPath, fileMsg.getData());
|
||||||
|
|
||||||
// TODO: Forward the received file-message to the destination user.
|
ClientSession receiver = userManager.getUser(fileMsg.getReceiver());
|
||||||
|
receiver.sendObject(fileMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void broadcastUserList() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
String activeUsers = userManager.listUsers();
|
||||||
|
ChatMessage updateMsg = new ChatMessage(
|
||||||
|
MessageType.USER_LIST,
|
||||||
|
"[server]",
|
||||||
|
null,
|
||||||
|
activeUsers
|
||||||
|
);
|
||||||
|
for (ClientSession session : userManager.getAllSessions()) {
|
||||||
|
session.sendObject(updateMsg);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.out.println("Error broadcasting user list: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ public class UserManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String listUsers() {
|
public String listUsers() {
|
||||||
return users.keySet().toString();
|
return String.join(",", users.keySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Iterable<ClientSession> getAllSessions() {
|
public Iterable<ClientSession> getAllSessions() {
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
.root {
|
||||||
|
-fx-font-family: "Segoe UI", "Helvetica", sans-serif;
|
||||||
|
-fx-background-color: #1e1e2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar {
|
||||||
|
-fx-background-color: #262638;
|
||||||
|
-fx-padding: 5 10 5 10;
|
||||||
|
-fx-alignment: CENTER_LEFT;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
-fx-text-fill: #a0a0c0;
|
||||||
|
-fx-font-size: 13px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-button {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-text-fill: white;
|
||||||
|
-fx-font-size: 14px;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-button:hover {
|
||||||
|
-fx-background-color: #3b3b55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-button-close:hover {
|
||||||
|
-fx-background-color: #e81123;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.label {
|
||||||
|
-fx-text-fill: #d1d1e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-field {
|
||||||
|
-fx-background-color: #2a2a3d;
|
||||||
|
-fx-text-fill: white;
|
||||||
|
-fx-background-radius: 6;
|
||||||
|
-fx-border-color: #3c3c55;
|
||||||
|
-fx-padding: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
-fx-background-color: #4da3ff;
|
||||||
|
-fx-text-fill: white;
|
||||||
|
-fx-background-radius: 6;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
-fx-padding: 6 15 6 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
-fx-background-color: #61afff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-view, .text-area {
|
||||||
|
-fx-background-color: #262638;
|
||||||
|
-fx-control-inner-background: #262638;
|
||||||
|
-fx-text-fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.attach-button{
|
||||||
|
-fx-font-size:16px;
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-button:hover{
|
||||||
|
-fx-background-color:#3b3b55;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
-fx-padding: 20;
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
|
||||||
|
<BorderPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
|
||||||
|
fx:controller="com.university.chat.Client.UI.ChatController">
|
||||||
|
|
||||||
|
<top>
|
||||||
|
<BorderPane fx:id="titleBar" styleClass="title-bar">
|
||||||
|
<center><Label text="UniChat" styleClass="title-text"/></center>
|
||||||
|
<right>
|
||||||
|
<HBox>
|
||||||
|
<Button text="—" styleClass="title-button" onAction="#minimize"/>
|
||||||
|
<Button text="□" styleClass="title-button" onAction="#maximize"/>
|
||||||
|
<Button text="✕" styleClass="title-button, title-button-close" onAction="#closeApp"/>
|
||||||
|
</HBox>
|
||||||
|
</right>
|
||||||
|
</BorderPane>
|
||||||
|
</top>
|
||||||
|
|
||||||
|
<center>
|
||||||
|
|
||||||
|
<HBox styleClass="content-wrapper" spacing="20">
|
||||||
|
<VBox spacing="10" prefWidth="200">
|
||||||
|
<Label text="Online Users"/>
|
||||||
|
<ListView fx:id="usersList" VBox.vgrow="ALWAYS"/>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<VBox spacing="10" HBox.hgrow="ALWAYS">
|
||||||
|
<Label text="Private Chat"/>
|
||||||
|
<AnchorPane fx:id="chatAnchor" style="-fx-background-color: #262638; -fx-background-radius: 8;" VBox.vgrow="ALWAYS"/>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<VBox spacing="10" prefWidth="250">
|
||||||
|
<Label text="Public Chat"/>
|
||||||
|
<TextArea fx:id="publicChatArea" editable="false" VBox.vgrow="ALWAYS"/>
|
||||||
|
|
||||||
|
<VBox fx:id="fileTransferBox" spacing="4" visible="false" managed="false">
|
||||||
|
<Label fx:id="fileTransferLabel" text="Sending file..."/>
|
||||||
|
<ProgressBar fx:id="fileTransferProgress" progress="0.0" prefWidth="260"/>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<HBox spacing="6">
|
||||||
|
<Button text="📎" onAction="#sendFile"/>
|
||||||
|
<TextField fx:id="messageField" HBox.hgrow="ALWAYS" promptText="Message..."/>
|
||||||
|
<Button text="Send" onAction="#sendPublicMessage"/>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
</VBox>
|
||||||
|
</HBox>
|
||||||
|
</center>
|
||||||
|
</BorderPane>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
|
||||||
|
<BorderPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
|
||||||
|
fx:controller="com.university.chat.Client.UI.LoginController">
|
||||||
|
|
||||||
|
<top>
|
||||||
|
<BorderPane styleClass="title-bar">
|
||||||
|
<center><Label text="Login" styleClass="title-text"/></center>
|
||||||
|
<right>
|
||||||
|
<HBox fx:id="titleBar">
|
||||||
|
<Button text="—" styleClass="title-button" onAction="#minimize"/>
|
||||||
|
<Button text="▢" styleClass="title-button" onAction="#maximize"/>
|
||||||
|
<Button text="✕" styleClass="title-button, title-button-close" onAction="#closeApp"/>
|
||||||
|
</HBox>
|
||||||
|
</right>
|
||||||
|
</BorderPane>
|
||||||
|
</top>
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<VBox alignment="CENTER" maxWidth="300" spacing="15">
|
||||||
|
<Label text="Welcome to UniChat" style="-fx-font-size: 20px; -fx-font-weight: bold;"/>
|
||||||
|
<TextField fx:id="usernameField" promptText="Enter your username"/>
|
||||||
|
<Button fx:id="nameButton" text="Connect" maxWidth="Infinity" onAction="#onConnectClicked"/>
|
||||||
|
</VBox>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
</BorderPane>
|
||||||
Reference in New Issue
Block a user