diff --git a/HelloController.java b/HelloController.java new file mode 100644 index 00000000..c3fe4a91 --- /dev/null +++ b/HelloController.java @@ -0,0 +1,74 @@ +package com.example; + +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.*; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +public class HelloController { + + + @FXML + private Button logoutButton; + @FXML + private AnchorPane scenePane; + @FXML + private ListView chatList; + @FXML + private TextField messageField; + @FXML + private TextField usernameField; + + private NtfyClient ntfy; + private final DateTimeFormatter timeFormat = DateTimeFormatter.ofPattern("HH:mm"); + + + @FXML + public void initialize() { + // Set your topic name (you can make this configurable) + String topic = "myfxchat"; + ntfy = new NtfyClient(topic); + + // Start listening to the topic + ntfy.subscribe(message -> Platform.runLater(() -> chatList.getItems().add(message))); + } + + @FXML + private void handleSend() { + String username = usernameField.getText().isBlank() ? "Anonymous" : usernameField.getText(); + String message = messageField.getText().trim(); + if (message.isEmpty()) return; + + String time = LocalTime.now().format(timeFormat); + String formatted = String.format("[%s] %s: %s", time, username, message); + chatList.getItems().add(formatted); + + ntfy.sendMessage(username, message); + messageField.clear(); + } + + Stage stage; + public void logout(ActionEvent event) { + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Log out"); + alert.setHeaderText("Log out"); + alert.setContentText("Are you sure you want to logout?"); + + if(alert.showAndWait().get() == ButtonType.OK){ + stage = (Stage) scenePane.getScene().getWindow(); + System.out.println("You have been logged out"); + stage.close(); + } + } +} + + + + + + diff --git a/pom.xml b/pom.xml index c40f667e..0b94de6a 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,16 @@ ${junit.jupiter.version} test + + io.github.cdimascio + dotenv-java + 3.0.0 + + + org.json + json + 20240303 + org.assertj assertj-core diff --git a/src/main/java/com/example/HelloController.java b/src/main/java/com/example/HelloController.java index fdd160a0..86f474bc 100644 --- a/src/main/java/com/example/HelloController.java +++ b/src/main/java/com/example/HelloController.java @@ -1,22 +1,95 @@ package com.example; +import javafx.application.Platform; +import javafx.event.ActionEvent; import javafx.fxml.FXML; -import javafx.scene.control.Label; +import javafx.scene.control.*; +import javafx.scene.layout.AnchorPane; +import javafx.stage.FileChooser; +import javafx.stage.Stage; + +import java.io.File; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; -/** - * Controller layer: mediates between the view (FXML) and the model. - */ public class HelloController { - private final HelloModel model = new HelloModel(); @FXML - private Label messageLabel; + private Button logoutButton; + @FXML + private AnchorPane scenePane; + @FXML + private ListView chatList; + @FXML + private TextField messageField; + @FXML + private TextField usernameField; + + + + private NtfyClient ntfy; + private final DateTimeFormatter timeFormat = DateTimeFormatter.ofPattern("HH:mm"); + + + + @FXML + public void initialize() { + // Set your topic name (you can make this configurable) + String topic = "myfxchat"; + ntfy = new NtfyClient(topic); + + // Start listening to the topic + ntfy.subscribe(message -> Platform.runLater(() -> chatList.getItems().add(message))); + } @FXML - private void initialize() { - if (messageLabel != null) { - messageLabel.setText(model.getGreeting()); + private void handleSend() { + String username = usernameField.getText().isBlank() ? "Anonymous" : usernameField.getText(); + String message = messageField.getText().trim(); + if (message.isEmpty()) return; + + String time = LocalTime.now().format(timeFormat); + String formatted = String.format("[%s] %s: %s", time, username, message); + chatList.getItems().add(formatted); + + ntfy.sendMessage(username, message); + messageField.clear(); + } + @FXML + private void handleAttachFile(ActionEvent event) { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Select a file to send"); + + File selectedFile = fileChooser.showOpenDialog(null); + + if (selectedFile != null) { + System.out.println("Selected file: " + selectedFile.getAbsolutePath()); + sendFileToBackend(selectedFile); + } + } + + private void sendFileToBackend(File selectedFile) { + } + + Stage stage; + public void logout(ActionEvent event) { + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Log out"); + alert.setHeaderText("Log out"); + alert.setContentText("Are you sure you want to logout?"); + + if(alert.showAndWait().get() == ButtonType.OK){ + stage = (Stage) scenePane.getScene().getWindow(); + System.out.println("You have been logged out"); + stage.close(); } + } } + + + + + + diff --git a/src/main/java/com/example/HelloController2.java b/src/main/java/com/example/HelloController2.java new file mode 100644 index 00000000..de804be2 --- /dev/null +++ b/src/main/java/com/example/HelloController2.java @@ -0,0 +1,16 @@ +package com.example; + +import javafx.fxml.FXML; + +import javafx.scene.control.Label; + + +public class HelloController2 { + + @FXML + private Label nameLabel; + + public void displayName(String userName) { + nameLabel.setText("Hello: " + userName); + } +} diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java index 96bdc5ca..835466ba 100644 --- a/src/main/java/com/example/HelloFX.java +++ b/src/main/java/com/example/HelloFX.java @@ -1,23 +1,99 @@ package com.example; + import javafx.application.Application; +import javafx.event.ActionEvent; 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.ButtonType; +import javafx.stage.FileChooser; import javafx.stage.Stage; +import java.io.File; +import java.io.FileInputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Objects; + public class HelloFX extends Application { @Override public void start(Stage stage) throws Exception { - FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml")); + // Load the FXML file (absolute path is safer) + FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("/com/example/hello-view.fxml")); + //FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/example/hello-view2.fxml")); + stage.setTitle("JavaFX Chat (ntfy)"); Parent root = fxmlLoader.load(); + + // Create the scene Scene scene = new Scene(root, 640, 480); + + // Add the stylesheet (optional) + scene.getStylesheets().add( + Objects.requireNonNull( + getClass().getResource("/application.css"), + "Missing resource: application.css" + ).toExternalForm() + ); + + // Show the window stage.setTitle("Hello MVC"); + stage.setScene(scene); stage.show(); + + stage.setOnCloseRequest(event -> { + event.consume(); + logout(stage); + + }); } + private void sendFileToBackend(File file) { + try { + String backendUrl = System.getenv("BACKEND_URL"); + if (backendUrl == null) { + System.err.println("BACKEND_URL not set"); + return; + } + + HttpURLConnection conn = (HttpURLConnection) new URL(backendUrl).openConnection(); + conn.setDoOutput(true); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/octet-stream"); + conn.setRequestProperty("Title", file.getName()); + + try (OutputStream os = conn.getOutputStream(); + FileInputStream fis = new FileInputStream(file)) { + fis.transferTo(os); + } + + int responseCode = conn.getResponseCode(); + System.out.println("Upload response: " + responseCode); + conn.disconnect(); + } catch (Exception e) { + e.printStackTrace(); + } + } + public void logout(Stage stage) { + + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Logout"); + alert.setHeaderText("You are about to log out"); + alert.setContentText("Do you want to save before exiting?"); + + if(alert.showAndWait().get() == ButtonType.OK){ + //stage = (Stage) scenePane.getScene().getWindow(); + System.out.println("You successfully logged out"); + stage.close(); + } + } + + public static void main(String[] args) { launch(); } diff --git a/src/main/java/com/example/HelloModel.java b/src/main/java/com/example/HelloModel.java index 385cfd10..cc73ff2f 100644 --- a/src/main/java/com/example/HelloModel.java +++ b/src/main/java/com/example/HelloModel.java @@ -1,15 +1,24 @@ package com.example; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import java.util.List; +import java.util.ArrayList; + /** * Model layer: encapsulates application data and business logic. */ public class HelloModel { - /** - * Returns a greeting based on the current Java and JavaFX versions. - */ - public String getGreeting() { - String javaVersion = System.getProperty("java.version"); - String javafxVersion = System.getProperty("javafx.version"); - return "Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + "."; + + private final List messages = new ArrayList<>(); + + public void addMessage(String message) { + messages.add(message); } + + public List getMessages() { + return messages; + + } } diff --git a/src/main/java/com/example/NtfyClient.java b/src/main/java/com/example/NtfyClient.java new file mode 100644 index 00000000..464fb938 --- /dev/null +++ b/src/main/java/com/example/NtfyClient.java @@ -0,0 +1,100 @@ +package com.example; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import io.github.cdimascio.dotenv.Dotenv; +import org.json.JSONObject; + +public class NtfyClient { + private final String baseUrl; + private final String topic; + private final HttpClient client; + + public NtfyClient(String topic) { + + Dotenv dotenv = Dotenv.load(); + String backendUrl = "https://ntfy.sh/defaulttopic"; + this.baseUrl = dotenv.get("BACKEND_URL"); + System.out.println("Backend URL: " + backendUrl); + + try { + dotenv = Dotenv.load(); + if (dotenv.get("BACKEND_URL") != null) { + backendUrl = dotenv.get("BACKEND_URL"); + } + } catch (Exception e) { + System.err.println("⚠️ Could not load .env file, using default URL."); + } + + this.topic = topic; + this.client = HttpClient.newHttpClient(); + } + + + public void sendMessage(String username, String message) { + try { + String json = String.format(""" + { + "topic": "%s", + "message": "%s", + "title": "%s" + } + """, topic, message.replace("\"", "\\\""), username); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("https://ntfy.sh")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(json)) + .build(); + + client.sendAsync(request, HttpResponse.BodyHandlers.discarding()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + public void subscribe(MessageHandler handler) { + new Thread(() -> { + while (true) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("https://ntfy.sh/" + topic + "/json")) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.body()))) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.isBlank()) { + JSONObject obj = new JSONObject(line); + if ("message".equals(obj.optString("event"))) { + String user = obj.optString("title", "Anonymous"); + String msg = obj.optString("message", ""); + handler.onMessage(user + ": " + msg); + } + } + } + } + } catch (Exception e) { + System.out.println("⚠️ Stream disconnected, retrying..."); + try { + Thread.sleep(2000); + } catch (InterruptedException ignored) {} + } + } + }, "Ntfy-Listener").start(); + } + + public interface MessageHandler { + void onMessage(String message); + } +} diff --git a/src/main/java/com/example/test.java b/src/main/java/com/example/test.java new file mode 100644 index 00000000..90bc813b --- /dev/null +++ b/src/main/java/com/example/test.java @@ -0,0 +1,8 @@ +package com.example; + +public class test { + + static void main() { + + } + } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 71574a27..5457213b 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,6 +1,12 @@ module hellofx { requires javafx.controls; requires javafx.fxml; + requires java.desktop; + requires javafx.base; + requires java.net.http; + requires jdk.hotspot.agent; + requires org.json; + requires io.github.cdimascio.dotenv.java; opens com.example to javafx.fxml; exports com.example; diff --git a/src/main/resources/application.css b/src/main/resources/application.css new file mode 100644 index 00000000..a8e7abf8 --- /dev/null +++ b/src/main/resources/application.css @@ -0,0 +1,32 @@ +.text-field { + -fx-font-size: 14px; +} + +.button { + -fx-background-color: #4CAF50; + -fx-text-fill: white; + -fx-background-radius: 8; +} + +.label { + -fx-font-weight: bold; +} + +.list-cell { + -fx-font-family: "Segoe UI"; + -fx-font-size: 14px; + -fx-padding: 8px; + -fx-text-fill: #222; +} + +.list-view { + -fx-background-color: #f9f9f9; + -fx-background-radius: 10; + -fx-border-radius: 10; + -fx-border-color: #ddd; +} + +#logOut{ + -fx-background-color: red; +} + diff --git a/src/main/resources/com/example/hello-view.fxml b/src/main/resources/com/example/hello-view.fxml index 20a7dc82..711017ee 100644 --- a/src/main/resources/com/example/hello-view.fxml +++ b/src/main/resources/com/example/hello-view.fxml @@ -1,9 +1,32 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/com/example/hello-view2.fxml b/src/main/resources/com/example/hello-view2.fxml new file mode 100644 index 00000000..aff0890f --- /dev/null +++ b/src/main/resources/com/example/hello-view2.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/style.css b/src/main/resources/style.css new file mode 100644 index 00000000..e69de29b diff --git a/src/main/resources/style.css.fxml b/src/main/resources/style.css.fxml new file mode 100644 index 00000000..b2521ad3 --- /dev/null +++ b/src/main/resources/style.css.fxml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/src/test/com/example/HelloModelTest.java b/src/test/com/example/HelloModelTest.java new file mode 100644 index 00000000..3334f503 --- /dev/null +++ b/src/test/com/example/HelloModelTest.java @@ -0,0 +1,50 @@ +package com.example; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class HelloModelTest { + + private HelloModel model; + + @BeforeEach + void setUp() { + model = new HelloModel(); + } + + @Test + void testAddMessageStoresMessages() { + model.addMessage("Hello"); + model.addMessage("World"); + + List messages = model.getMessages(); + + assertEquals(2, messages.size(), "Should store exactly two messages"); + assertEquals("Hello", messages.get(0), "First message should match"); + assertEquals("World", messages.get(1), "Second message should match"); + } + + @Test + void testGetMessagesReturnsListReference() { + List messages = model.getMessages(); + assertNotNull(messages, "getMessages() should never return null"); + } + + @Test + void testMessagesListIsMutable() { + + List messages = model.getMessages(); + messages.add("Direct modification"); + + assertEquals(1, model.getMessages().size()); + } + + @Test + void testEmptyInitially() { + assertTrue(model.getMessages().isEmpty(), "New model should start with an empty message list"); + } +}