From 7b548354687dec84d6c4f3c193f87417973506de Mon Sep 17 00:00:00 2001 From: Asal Lotfi Date: Tue, 2 Sep 2025 01:27:18 +0330 Subject: [PATCH] New Channel UI implemented. --- .../UI/AddMembersController.java | 5 - .../UI/AddSubscriberController.java | 226 ++++++++++++++++++ .../UI/NewChannelController.java | 146 +++++++++++ .../UI/NewGroupController.java | 5 - .../UI/SidebarMenuController.java | 14 +- .../telegramfinalproject/CSS/dark_theme.css | 45 ++++ .../telegramfinalproject/CSS/light_theme.css | 44 ++++ .../Fxml/add_subscriber.fxml | 69 ++++++ .../Fxml/new_channel.fxml | 62 +++++ 9 files changed, 605 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/to/telegramfinalproject/UI/AddSubscriberController.java create mode 100644 src/main/java/org/to/telegramfinalproject/UI/NewChannelController.java create mode 100644 src/main/resources/org/to/telegramfinalproject/Fxml/add_subscriber.fxml create mode 100644 src/main/resources/org/to/telegramfinalproject/Fxml/new_channel.fxml diff --git a/src/main/java/org/to/telegramfinalproject/UI/AddMembersController.java b/src/main/java/org/to/telegramfinalproject/UI/AddMembersController.java index a6a038f..c343e80 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/AddMembersController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/AddMembersController.java @@ -239,11 +239,6 @@ public class AddMembersController { searchIcon.setGraphic(icon); } - private void showAlert(String msg) { - Alert alert = new Alert(Alert.AlertType.WARNING, msg, ButtonType.OK); - alert.showAndWait(); - } - public void setGroupInfo(String groupName, File groupImageFile) { this.groupName = groupName; this.groupImageFile = groupImageFile; diff --git a/src/main/java/org/to/telegramfinalproject/UI/AddSubscriberController.java b/src/main/java/org/to/telegramfinalproject/UI/AddSubscriberController.java new file mode 100644 index 0000000..ad0703d --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/UI/AddSubscriberController.java @@ -0,0 +1,226 @@ +package org.to.telegramfinalproject.UI; + +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; + +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; + +public class AddSubscriberController { + + @FXML private VBox contactsList; + @FXML private VBox addMembersCard; + @FXML private TextField searchField; + @FXML private Pane overlayBackground; + @FXML private ScrollPane contactsScroll; + @FXML private Button searchIcon; + @FXML private Label memberCountLabel; + @FXML private FlowPane selectedMembersPane; + @FXML private Button skipButton; + @FXML private Button addButton; + + // Keep selected contacts + private final Set selectedContacts = new HashSet<>(); + + private String channelName; + private File channelImageFile; + private String description; + + // Sample data for testing + private final List allContacts = Arrays.asList( + new Contact("Ali", "last seen recently", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), + new Contact("Iman", "last seen a long time ago", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), + new Contact("Amir", "last seen within a month", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), + new Contact("Sara", "online", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), + new Contact("Ali", "last seen recently", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), + new Contact("Iman", "last seen a long time ago", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), + new Contact("Amir", "last seen within a month", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), + new Contact("Sara", "online", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), + new Contact("Ali", "last seen recently", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), + new Contact("Iman", "last seen a long time ago", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), + new Contact("Amir", "last seen within a month", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), + new Contact("Sara", "online", "/org/to/telegramfinalproject/Avatars/default_user_profile.png") + ); + + @FXML + public void initialize() { + updateMemberCount(); + + // Sort + render initially + renderContacts(allContacts.stream() + .sorted(Comparator.comparing(Contact::getName)) + .collect(Collectors.toList())); + + // Search filter + searchField.textProperty().addListener((obs, oldVal, newVal) -> { + String filter = newVal.toLowerCase(); + List filtered = allContacts.stream() + .filter(c -> c.getName().toLowerCase().contains(filter)) + .sorted(Comparator.comparing(Contact::getName)) + .collect(Collectors.toList()); + renderContacts(filtered); + }); + + // Auto-focus + Platform.runLater(() -> searchField.requestFocus()); + + // Skip → ignore selection and create chat + skipButton.setOnAction(e -> { + createChannel(); // always creates, even if no members selected + }); + + // Add → requires at least one member + addButton.setOnAction(e -> { + if (!selectedContacts.isEmpty()) { + createChannel(); + } + }); + + // Close when clicking outside + overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent())); + + // Smooth scroll + contactsScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()); + contactsScroll.setPannable(true); + contactsScroll.setFitToWidth(true); + contactsScroll.setFitToHeight(false); + contactsScroll.getContent().setOnScroll(event -> { + double deltaY = event.getDeltaY() * 0.003; + contactsScroll.setVvalue(contactsScroll.getVvalue() - deltaY); + }); + + // Theme icon handling + Platform.runLater(() -> { + if (addMembersCard.getScene() != null) { + ThemeManager.getInstance().registerScene(addMembersCard.getScene()); + } + }); + ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> updateSearchIcon(newVal)); + updateSearchIcon(ThemeManager.getInstance().isDarkMode()); + } + + private void createChannel() { + // TODO: real server request → for now just print and close overlay + System.out.println("✅ Creating group/channel: " + channelName); + System.out.println("Selected members: " + selectedContacts.stream() + .map(Contact::getName).collect(Collectors.joining(", "))); + + MainController.getInstance().closeOverlay(addMembersCard.getParent()); + + // TODO: open chat immediately (like you did with newGroup) + } + + private void renderContacts(List contacts) { + contactsList.getChildren().clear(); + + if (contacts.isEmpty()) { + StackPane emptyPane = new StackPane(); + emptyPane.setPrefHeight(300); + emptyPane.setAlignment(Pos.CENTER); + + Label emptyLabel = new Label("No contacts found"); + emptyLabel.getStyleClass().add("no-contacts-label"); + + emptyPane.getChildren().add(emptyLabel); + contactsList.getChildren().add(emptyPane); + return; + } + + for (Contact c : contacts) { + HBox item = new HBox(10); + item.getStyleClass().add("contact-item"); + + // Avatar + ImageView avatar = new ImageView(new Image( + Objects.requireNonNull(getClass().getResourceAsStream(c.getImageUrl())) + )); + avatar.setFitWidth(48); + avatar.setFitHeight(48); + avatar.setPreserveRatio(true); + + VBox details = new VBox(2); + Label nameLabel = new Label(c.getName()); + nameLabel.getStyleClass().add("contact-name"); + Label statusLabel = new Label(c.getStatus()); + statusLabel.getStyleClass().add("contact-status"); + + details.getChildren().addAll(nameLabel, statusLabel); + + item.getChildren().addAll(avatar, details); + + // Click to toggle selection + item.setOnMouseClicked(e -> toggleSelection(c)); + + // Highlight if already selected + if (selectedContacts.contains(c)) { + item.getStyleClass().add("contact-selected"); + } + + contactsList.getChildren().add(item); + } + } + + private void toggleSelection(Contact contact) { + if (selectedContacts.contains(contact)) { + selectedContacts.remove(contact); + } else { + selectedContacts.add(contact); + } + updateMemberCount(); + updateSelectedMembersPane(); + renderContacts(allContacts); + } + + private void updateSelectedMembersPane() { + selectedMembersPane.getChildren().clear(); + for (Contact c : selectedContacts) { + Label chip = new Label(c.getName()); + chip.getStyleClass().add("member-chip"); + selectedMembersPane.getChildren().add(chip); + } + } + + private void updateMemberCount() { + memberCountLabel.setText(selectedContacts.size() + " / 200000"); + } + + private void updateSearchIcon(boolean darkMode) { + String iconPath = darkMode + ? "/org/to/telegramfinalproject/Icons/search_light.png" + : "/org/to/telegramfinalproject/Icons/search_dark.png"; + + ImageView icon = new ImageView(new Image(getClass().getResourceAsStream(iconPath))); + icon.setFitWidth(16); + icon.setFitHeight(16); + searchIcon.setGraphic(icon); + } + + public void setChannelInfo(String name, String description, File image) { + this.channelName = name; + this.channelImageFile = image; + this.description = description; + } + + // Inner class for contact data + public static class Contact { + private final String name; + private final String status; + private final String imageUrl; + + public Contact(String name, String status, String imageUrl) { + this.name = name; + this.status = status; + this.imageUrl = imageUrl; + } + + public String getName() { return name; } + public String getStatus() { return status; } + public String getImageUrl() { return imageUrl; } + } +} diff --git a/src/main/java/org/to/telegramfinalproject/UI/NewChannelController.java b/src/main/java/org/to/telegramfinalproject/UI/NewChannelController.java new file mode 100644 index 0000000..a13ac44 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/UI/NewChannelController.java @@ -0,0 +1,146 @@ +package org.to.telegramfinalproject.UI; + +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import javafx.scene.layout.Pane; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +public class NewChannelController { + + @FXML private VBox newChannelCard; + @FXML private Pane overlayBackground; + @FXML private TextField channelNameField; + @FXML private Label channelNameLabel; + @FXML private TextArea channelDescField; + @FXML private Label channelDescLabel; + @FXML private Button cameraButton; + @FXML private ImageView cameraIcon; + @FXML private Button cancelButton; + @FXML private Button createButton; + @FXML private StackPane overlayRoot; // the root + @FXML private Label descCounter; + + private File channelImageFile; + + @FXML + public void initialize() { + // Load default camera icon + cameraIcon.setImage(new Image( + getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/camera.png") + )); + + // Camera button action → choose image + cameraButton.setOnAction(e -> { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Choose Channel Picture"); + chooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("Image Files", "*.png", "*.jpg", "*.jpeg") + ); + File file = chooser.showOpenDialog(cameraButton.getScene().getWindow()); + if (file != null) { + channelImageFile = file; + cameraIcon.setImage(new Image(file.toURI().toString())); + } + }); + + // Cancel → close overlay + cancelButton.setOnAction(e -> { + MainController.getInstance().closeOverlay(overlayRoot); + }); + + // Close when clicking outside card + overlayBackground.setOnMouseClicked(e -> { + MainController.getInstance().closeOverlay(overlayRoot); + }); + + // Create button → validate name + submit + createButton.setOnAction(e -> { + String channelName = channelNameField.getText().trim(); + String description = channelDescField.getText().trim(); + + if (channelName.isEmpty()) { + // Apply error style + channelNameField.getStyleClass().add("error"); + channelNameLabel.getStyleClass().add("error"); + return; + } + + try { + // Load Add Members overlay + FXMLLoader loader = new FXMLLoader(getClass().getResource( + "/org/to/telegramfinalproject/Fxml/add_subscriber.fxml")); + StackPane addSubscribersOverlay = loader.load(); + + // Pass channel info to AddMembersController + AddSubscriberController controller = loader.getController(); + controller.setChannelInfo(channelName, description, channelImageFile); + + // Close the current "New Channel" overlay + MainController.getInstance().closeOverlay(overlayRoot); + + // Then open the "Add Members" overlay + MainController.getInstance().showOverlay(addSubscribersOverlay); + + } catch (IOException ex) { + ex.printStackTrace(); + } + }); + + final int MAX_LENGTH = 255; + channelDescField.addEventFilter(javafx.scene.input.KeyEvent.KEY_TYPED, e -> { + if (channelDescField.getText().length() >= MAX_LENGTH) { + e.consume(); // stop extra character from being typed + } + }); + + channelDescField.textProperty().addListener((obs, oldText, newText) -> { + if (newText.length() > MAX_LENGTH) { + channelDescField.setText(newText.substring(0, MAX_LENGTH)); + channelDescField.positionCaret(MAX_LENGTH); + } + + // Current length out of max + int current = channelDescField.getText().length(); + descCounter.setText(current + " / " + MAX_LENGTH); + + // Style when limit reached + if (current == MAX_LENGTH) { + descCounter.setStyle("-fx-text-fill: red;"); + } else { + descCounter.setStyle(""); // fallback to CSS + } + }); + + // Initial value + descCounter.setText("0 / " + MAX_LENGTH); + + // Reset error state when typing + channelNameField.textProperty().addListener((obs, oldVal, newVal) -> { + if (!newVal.trim().isEmpty()) { + channelNameField.getStyleClass().remove("error"); + channelNameLabel.getStyleClass().remove("error"); + } + }); + + // Auto-focus channel name on open + Platform.runLater(() -> channelNameField.requestFocus()); + + // Register scene for ThemeManager → stylesheet swap will handle colors/icons + Platform.runLater(() -> { + if (newChannelCard.getScene() != null) { + ThemeManager.getInstance().registerScene(newChannelCard.getScene()); + } + }); + } +} diff --git a/src/main/java/org/to/telegramfinalproject/UI/NewGroupController.java b/src/main/java/org/to/telegramfinalproject/UI/NewGroupController.java index ba0a533..a676fb2 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/NewGroupController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/NewGroupController.java @@ -97,9 +97,4 @@ public class NewGroupController { // Auto_focus search bar when overlay opens Platform.runLater(() -> groupNameField.requestFocus()); } - - private void showAlert(String msg) { - Alert alert = new Alert(Alert.AlertType.WARNING, msg, ButtonType.OK); - alert.showAndWait(); - } } diff --git a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java index ab1a43f..08208fc 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java @@ -210,7 +210,19 @@ public class SidebarMenuController { } } - private void createNewChannel() { System.out.println("Creating New Channel..."); } + private void createNewChannel() { + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource( + "/org/to/telegramfinalproject/Fxml/new_channel.fxml")); + Node channelOverlay = loader.load(); + + // Show overlay (like MyProfile, Contacts, etc.) + MainController.getInstance().showOverlay(channelOverlay); + + } catch (IOException e) { + e.printStackTrace(); + } + } private void openContacts() { try { diff --git a/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css b/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css index 53e5618..60b6b8c 100644 --- a/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css +++ b/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css @@ -715,3 +715,48 @@ .member-chip:hover { -fx-background-color: #31475b; /* lighter on hover */ } + +.desc-counter { + -fx-font-size: 11px; + -fx-text-fill: #aaa; /* slightly lighter gray for dark background */ + -fx-alignment: CENTER_RIGHT; +} + +/* ===== TextArea Scrollbar Styling ===== */ +.text-area .scroll-pane .scroll-bar:vertical { + -fx-pref-width: 6px; /* super thin */ + -fx-background-color: transparent; + -fx-opacity: 0; /* hidden by default */ + -fx-padding: 0; +} + +.text-area .scroll-pane:hover .scroll-bar:vertical { + -fx-opacity: 0.6; /* appear on hover */ + -fx-transition: opacity 0.3s ease; +} + +.text-area .scroll-pane .thumb { + -fx-background-color: rgba(120,120,120,0.5); /* subtle gray */ + -fx-background-radius: 2; +} + +.text-area .scroll-pane .scroll-bar:horizontal { + -fx-opacity: 0; /* hide horizontal completely */ + -fx-max-height: 0; +} + +/* Channel description text area */ +.channel-desc-area { + -fx-background-color: #1f2a36; /* dark background */ + -fx-text-fill: #eee; /* text color */ + -fx-control-inner-background: #1f2a36; /* inner background */ + -fx-prompt-text-fill: #888; /* placeholder */ + -fx-border-color: #444; + -fx-border-radius: 6; + -fx-background-radius: 6; +} + +/* Fix for the inner content region */ +.channel-desc-area .content { + -fx-background-color: #1f2a36; +} diff --git a/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css b/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css index 25d533c..9421837 100644 --- a/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css +++ b/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css @@ -719,3 +719,47 @@ .member-chip:hover { -fx-background-color: #c6c6c6; /* slightly darker on hover */ } + +.desc-counter { + -fx-font-size: 11px; + -fx-text-fill: #888; /* subtle gray */ + -fx-alignment: CENTER_RIGHT; +} + +/* ===== TextArea Scrollbar Styling ===== */ +.text-area .scroll-pane .scroll-bar:vertical { + -fx-pref-width: 6px; /* super thin */ + -fx-background-color: transparent; + -fx-opacity: 0; /* hidden by default */ + -fx-padding: 0; +} + +.text-area .scroll-pane:hover .scroll-bar:vertical { + -fx-opacity: 0.6; /* appear on hover */ + -fx-transition: opacity 0.3s ease; +} + +.text-area .scroll-pane .thumb { + -fx-background-color: rgba(120,120,120,0.5); /* subtle gray */ + -fx-background-radius: 2; +} + +.text-area .scroll-pane .scroll-bar:horizontal { + -fx-opacity: 0; /* hide horizontal completely */ + -fx-max-height: 0; +} + +.channel-desc-area { + -fx-background-color: #fff; + -fx-text-fill: #000; + -fx-control-inner-background: #fff; + -fx-prompt-text-fill: #888; + -fx-border-color: #ccc; + -fx-border-radius: 6; + -fx-background-radius: 6; +} + +.channel-desc-area .content { + -fx-background-color: #fff; +} + diff --git a/src/main/resources/org/to/telegramfinalproject/Fxml/add_subscriber.fxml b/src/main/resources/org/to/telegramfinalproject/Fxml/add_subscriber.fxml new file mode 100644 index 0000000..71f354d --- /dev/null +++ b/src/main/resources/org/to/telegramfinalproject/Fxml/add_subscriber.fxml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +