diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java index 270cb94..e4e5d1e 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java @@ -34,7 +34,7 @@ public class ChatItemController { * @param unread Unread message count * @param imageUrl Path/URL of profile image (can be null/empty) */ - public void setChatData(String name, String lastMsg, String time, int unread, String imageUrl) { + public void setChatData(String name, String lastMsg, String time, int unread, String imageUrl, String chatType) { chatName.setText(name); lastMessage.setText(lastMsg); chatTime.setText(time); @@ -77,9 +77,17 @@ public class ChatItemController { profileImageUser.setManaged(true); } else { + String path; + if ("group".equalsIgnoreCase(chatType)) { + path = "/org/to/telegramfinalproject/Avatars/default_group_profile.png"; + } else if ("channel".equalsIgnoreCase(chatType)) { + path = "/org/to/telegramfinalproject/Avatars/default_channel_profile.png"; + } else { + path = "/org/to/telegramfinalproject/Avatars/default_user_profile.png"; + } + profileImageUser.setImage(new Image( - Objects.requireNonNull(getClass().getResourceAsStream( - "/org/to/telegramfinalproject/Icons/default_profile.png")) + Objects.requireNonNull(getClass().getResourceAsStream(path)) )); profileImageUser.setVisible(true); profileImageUser.setManaged(true); diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index aee726f..58d12fc 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -588,12 +588,10 @@ public class ChatPageController { return (id==null||id.isEmpty()) ? "Unknown" : id.substring(0, Math.min(8,id.length())); } - - private void markAsRead(ChatEntry entry) { JSONObject readReq = new JSONObject(); readReq.put("action", "mark_as_read"); - readReq.put("receiver_id", entry.getId().toString()); // ⛳️ internal_id + readReq.put("receiver_id", entry.getId().toString()); // internal_id readReq.put("receiver_type", entry.getType()); ActionHandler.sendWithResponse(readReq); } @@ -632,10 +630,6 @@ public class ChatPageController { // messageContainer.getChildren().add(row); // } - - - - private void addBubble( boolean outgoing, String displayName, @@ -801,7 +795,4 @@ public class ChatPageController { private static boolean notBlank(String s) { return s != null && !s.isBlank(); } private static String ellipsize(String s, int max) { return s.length() > max ? s.substring(0, max) + "…" : s; } - - - } diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index 132b5bc..2ecd909 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -9,6 +9,7 @@ import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; @@ -16,25 +17,30 @@ import javafx.util.Duration; import org.to.telegramfinalproject.Models.ChatEntry; import org.to.telegramfinalproject.Client.ActionHandler; import java.io.IOException; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; +import java.util.*; public class MainController { // === LEFT PANE === @FXML private VBox chatListContainer; // inside the scrollPane @FXML private ScrollPane scrollPane; // chat list scroll + + // === Global Search === + @FXML private TextField searchBar; + @FXML private VBox globalSearchPane; + @FXML private ListView globalSearchResults; + @FXML private VBox noResultsBox; + @FXML private ImageView noResultIcon; + private enum SearchMode { + GLOBAL, + CHAT + } + private SearchMode currentSearchMode = SearchMode.GLOBAL; + private UUID currentChatId; // if in CHAT mode, which chat to search in + + // === Search In Chat === @FXML private VBox chatSearchPane; // search results panel @FXML private ListView chatSearchResults; - @FXML private MenuButton scopeDropdown; - - // === TOP BAR === - @FXML private TextField searchBar; // global search bar - @FXML private Button menuButton; // === RIGHT PANE === @FXML private VBox leftPane; @@ -137,15 +143,104 @@ public class MainController { // } // }); - // وقتی کاربر Enter زد روی سرچ‌بار، درخواست سرچ بفرست - searchBar.setOnAction(e -> performGlobalSearch(searchBar.getText().trim())); + searchBar.setOnAction(e -> { + String keyword = searchBar.getText().trim(); + if (keyword.isEmpty()) return; - // باز/بسته کردن پنل نتایج با تایپ (دلخواه) - searchBar.textProperty().addListener((obs,o,n)->{ - if (n!=null && !n.isBlank()) showSearchPanel(); + if (currentSearchMode == SearchMode.GLOBAL) { + performGlobalSearch(keyword); + } else if (currentSearchMode == SearchMode.CHAT && currentChatId != null) { + performChatSearch(keyword, currentChatId); + } }); - // کلیک روی نتیجه + searchBar.setOnAction(e -> performGlobalSearch(searchBar.getText().trim())); + + searchBar.textProperty().addListener((obs,o,n)->{ + if (n!=null && !n.isBlank()) showGlobalSearchPanel(); + }); + + globalSearchResults.setOnMouseClicked(e -> { + int idx = globalSearchResults.getSelectionModel().getSelectedIndex(); + if (idx >= 0 && idx < searchBacking.size()) { + openSearchResult(searchBacking.get(idx)); + } + }); + + globalSearchResults.setCellFactory(list -> new ListCell<>() { + private final HBox container = new HBox(10); + private final ImageView avatar = new ImageView(); + private final VBox texts = new VBox(2); + private final Label title = new Label(); + private final Label subtitle = new Label(); + + { + avatar.setFitWidth(36); + avatar.setFitHeight(36); + avatar.getStyleClass().add("global-search-avatar"); + title.getStyleClass().add("global-search-title"); + subtitle.getStyleClass().add("global-search-subtitle"); + texts.getChildren().addAll(title, subtitle); + container.getChildren().addAll(avatar, texts); + } + + @Override + protected void updateItem(SearchResult r, boolean empty) { + super.updateItem(r, empty); + if (empty || r == null) { + setGraphic(null); + } else { + title.setText(r.title); + if (r.type == SRType.MESSAGE) { + subtitle.setText(r.subtitle != null ? r.subtitle : ""); + } else { + subtitle.setText("Press to see messages"); + } + + // Default profile images depending on type + switch (r.type) { + case USER: + avatar.setImage(new Image( + Objects.requireNonNull(getClass().getResourceAsStream( + "/org/to/telegramfinalproject/Avatars/default_user_profile.png" + )) + )); + break; + case GROUP: + avatar.setImage(new Image( + Objects.requireNonNull(getClass().getResourceAsStream( + "/org/to/telegramfinalproject/Avatars/default_group_profile.png" + )) + )); + break; + case CHANNEL: + avatar.setImage(new Image( + Objects.requireNonNull(getClass().getResourceAsStream( + "/org/to/telegramfinalproject/Avatars/default_channel_profile.png" + )) + )); + break; + case MESSAGE: + // For messages, use sender default (user style) + avatar.setImage(new Image( + Objects.requireNonNull(getClass().getResourceAsStream( + "/org/to/telegramfinalproject/Avatars/default_user_profile.png" + )) + )); + break; + } + + avatar.setFitWidth(50); + avatar.setFitHeight(50); + avatar.setPreserveRatio(true); + avatar.setSmooth(true); + + setGraphic(container); + } + } + }); + + // CLick on the search result chatSearchResults.setOnMouseClicked(e -> { int idx = chatSearchResults.getSelectionModel().getSelectedIndex(); if (idx >= 0 && idx < searchBacking.size()) { @@ -166,11 +261,25 @@ public class MainController { searchBar.requestFocus(); } + public void showGlobalSearchPanel() { + scrollPane.setVisible(false); + scrollPane.setManaged(false); + + chatSearchPane.setVisible(false); + chatSearchPane.setManaged(false); + + globalSearchPane.setVisible(true); + globalSearchPane.setManaged(true); + } + @FXML public void closeSearchPanel() { chatSearchPane.setVisible(false); chatSearchPane.setManaged(false); + currentSearchMode = SearchMode.GLOBAL; + currentChatId = null; + scrollPane.setVisible(true); scrollPane.setManaged(true); } @@ -282,7 +391,7 @@ public class MainController { Node chatItem = loader.load(); ChatItemController controller = loader.getController(); - cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), "/org/to/telegramfinalproject/Avatars/default_profile.png"); + cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), "/org/to/telegramfinalproject/Avatars/default_user_profile.png", chat.getType()); item.setOnMouseClicked(e -> openChat(chat)); chatListContainer.getChildren().add(item); @@ -325,6 +434,9 @@ public class MainController { ChatPageController controller = loader.getController(); controller.showChat(chat); + currentSearchMode = SearchMode.CHAT; + currentChatId = UUID.fromString(chat.getId().toString()); + chatDisplayArea.getChildren().setAll(chatPage); ChatItemController item = itemControllers.get(chat.getId()); @@ -441,11 +553,13 @@ public class MainController { private void updateIconsForDarkMode() { // Example: switch images to white versions menuIcon.setImage(new Image(getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/menu_light.png"))); + noResultIcon.setImage(new Image(getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/no_result_light.png"))); } private void updateIconsForLightMode() { // Example: switch images to black versions menuIcon.setImage(new Image(getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/menu_dark.png"))); + noResultIcon.setImage(new Image(getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/no_result_dark.png"))); } private void updateLabelsForDarkMode() { @@ -497,6 +611,49 @@ public class MainController { } } + private void performChatSearch(String keyword, UUID chatId) { + if (keyword == null || keyword.isBlank()) { + chatSearchResults.getItems().clear(); + return; + } + + showSearchPanel(); + + org.json.JSONObject req = new org.json.JSONObject(); + req.put("action", "search_in_chat"); + req.put("keyword", keyword); + req.put("chat_id", chatId.toString()); + req.put("user_id", org.to.telegramfinalproject.Client.Session.currentUser.getString("user_id")); + + new Thread(() -> { + org.json.JSONObject resp; + try { + resp = ActionHandler.sendWithResponse(req); + } catch (Exception ex) { + ex.printStackTrace(); + return; + } + + if (resp == null || !"success".equals(resp.optString("status"))) return; + org.json.JSONArray arr = resp.optJSONObject("data").optJSONArray("results"); + if (arr == null) arr = new org.json.JSONArray(); + + java.util.List tmp = new java.util.ArrayList<>(); + for (int i = 0; i < arr.length(); i++) { + org.json.JSONObject it = arr.getJSONObject(i); + String time = it.optString("time", ""); + String content = it.optString("content", "[No content]"); + tmp.add("🗨 " + content + (time.isEmpty() ? "" : " • " + time)); + } + + Platform.runLater(() -> { + chatSearchResults.getItems().setAll(tmp); + chatSearchResults.setVisible(true); + chatSearchResults.setManaged(true); + }); + }).start(); + } + public void performGlobalSearch(String keyword) { if (keyword == null || keyword.isBlank()) { @@ -510,7 +667,7 @@ public class MainController { return; } - showSearchPanel(); + showGlobalSearchPanel(); // درخواست به سرور (مثل کنسول) org.json.JSONObject req = new org.json.JSONObject(); @@ -597,17 +754,46 @@ public class MainController { private String subtitleSep(String s){ return s==null || s.isBlank()? "" : " — "; } - private void renderSearchResults(java.util.List results) { - searchBacking.clear(); - searchBacking.addAll(results); +// private void renderSearchResults(java.util.List results) { +// searchBacking.clear(); +// searchBacking.addAll(results); +// +// javafx.collections.ObservableList view = +// javafx.collections.FXCollections.observableArrayList(results); +// globalSearchResults.setItems(view); +// globalSearchResults.setVisible(true); +// globalSearchResults.setManaged(true); +// } - javafx.collections.ObservableList view = javafx.collections.FXCollections.observableArrayList(); - for (SearchResult r : results) view.add(r.toDisplay()); - chatSearchResults.setItems(view); - chatSearchResults.setVisible(true); - chatSearchResults.setManaged(true); + private void renderSearchResults(List results) { + globalSearchResults.getItems().clear(); + + if (results.isEmpty()) { + noResultsBox.setVisible(true); + noResultsBox.setManaged(true); + globalSearchResults.setVisible(false); + globalSearchResults.setManaged(false); + return; + } + + noResultsBox.setVisible(false); + noResultsBox.setManaged(false); + globalSearchResults.setVisible(true); + globalSearchResults.setManaged(true); + + globalSearchResults.getItems().addAll(results); } + @FXML + public void closeGlobalSearch() { + globalSearchPane.setVisible(false); + globalSearchPane.setManaged(false); + + scrollPane.setVisible(true); + scrollPane.setManaged(true); + + searchBar.clear(); + } private void openSearchResult(SearchResult r) { switch (r.type) { @@ -754,8 +940,4 @@ public class MainController { return false; } - - - - } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java index 6a5eff8..d1810ee 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java @@ -3,7 +3,6 @@ package org.to.telegramfinalproject.UI; import javafx.animation.TranslateTransition; import javafx.application.Platform; import javafx.fxml.FXML; -import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.image.Image; @@ -48,7 +47,7 @@ public class SidebarMenuController { @FXML public void initialize() { // Load default profile image - Image profile = loadImage("/org/to/telegramfinalproject/Avatars/default_profile.png"); + Image profile = loadImage("/org/to/telegramfinalproject/Avatars/default_user_profile.png"); if (profile != null) profileImage.setImage(profile); setupButtonActions(); @@ -197,7 +196,7 @@ public class SidebarMenuController { String img = user.optString("image_url", ""); Image pic = tryLoadImage(img); if (pic == null) { - pic = loadImage("/org/to/telegramfinalproject/Avatars/default_profile.png"); + pic = loadImage("/org/to/telegramfinalproject/Avatars/default_user_profile.png"); } if (pic != null) profileImage.setImage(pic); } diff --git a/src/main/resources/org/to/telegramfinalproject/Avatars/default_channel_profile.png b/src/main/resources/org/to/telegramfinalproject/Avatars/default_channel_profile.png new file mode 100644 index 0000000..f9343f6 Binary files /dev/null and b/src/main/resources/org/to/telegramfinalproject/Avatars/default_channel_profile.png differ diff --git a/src/main/resources/org/to/telegramfinalproject/Avatars/default_group_profile.png b/src/main/resources/org/to/telegramfinalproject/Avatars/default_group_profile.png new file mode 100644 index 0000000..997616c Binary files /dev/null and b/src/main/resources/org/to/telegramfinalproject/Avatars/default_group_profile.png differ diff --git a/src/main/resources/org/to/telegramfinalproject/Avatars/default_profile.png b/src/main/resources/org/to/telegramfinalproject/Avatars/default_user_profile.png similarity index 100% rename from src/main/resources/org/to/telegramfinalproject/Avatars/default_profile.png rename to src/main/resources/org/to/telegramfinalproject/Avatars/default_user_profile.png 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 8360317..5e00d84 100644 --- a/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css +++ b/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css @@ -1,3 +1,9 @@ +/* ===== Dark Theme Root ===== */ +.root { + -fx-background-color: #0f1c26; /* overall dark base */ + -fx-text-fill: #e8f1f8; +} + /* Sidebar buttons */ .sidebar-btn { -fx-background-color: transparent; @@ -170,7 +176,7 @@ .search-placeholder { -fx-text-fill: #8ea1b2; -fx-font-size: 13px; } /* ===== Global search bar ===== */ -.global-search { +.global-search-pane { -fx-background-color: #1b2b39; -fx-background-radius: 15; -fx-text-fill: #e8f1f8; @@ -294,3 +300,82 @@ -fx-background-radius: 10; -fx-font-weight: bold; } + +/* === Global Search List (Dark) === */ +.global-search-list { + -fx-background-color: #1b2735; /* uniform dark background */ + -fx-control-inner-background: #1b2735; + -fx-control-inner-background-alt: #1b2735; /* disable stripes */ +} + +.global-search-list .virtual-flow, +.global-search-list .clipped-container, +.global-search-list .sheet { + -fx-background-color: #1b2735; /* internal containers same */ +} + +.global-search-list .list-cell { + -fx-padding: 8 10; + -fx-background-color: #1b2735; /* solid dark rows */ + -fx-border-color: #2e3948; /* subtle divider */ + -fx-border-width: 0 0 0.5 0; + -fx-alignment: CENTER_LEFT; + -fx-text-fill: #e8f1f8; /* light text */ +} + +.global-search-list .list-cell:hover { + -fx-background-color: rgba(255, 255, 255, 0.08); /* lighter on hover */ +} + +.global-search-list .list-cell:selected { + -fx-background-color: #2a3a4a; /* darker blue-gray on select */ +} + +/* Titles & subtitles */ +.global-search-title { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: #e8f1f8; +} +.global-search-subtitle { + -fx-font-size: 12px; + -fx-text-fill: #8ea1b2; /* muted gray-blue */ +} + +/* Search header */ +.search-header { + -fx-padding: 8 12; + -fx-background-color: #1b2735; + -fx-border-color: #2e3948; + -fx-border-width: 0 0 1 0; +} +.search-header-label { + -fx-font-size: 13px; + -fx-font-weight: bold; + -fx-text-fill: #e8f1f8; +} + +/* No results */ +.no-results-label { + -fx-font-size: 12px; + -fx-text-fill: #8ea1b2; +} + +/* Avatar placeholders */ +.global-search-avatar { + -fx-background-radius: 50%; + -fx-background-color: #444; + -fx-min-width: 36; + -fx-min-height: 36; +} + +/* Close button */ +.search-close-btn { + -fx-background-color: transparent; + -fx-text-fill: #e8f1f8; + -fx-font-size: 14px; + -fx-cursor: hand; +} +.search-close-btn:hover { + -fx-text-fill: #2ca4ff; +} 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 f2b261d..5bf2e2e 100644 --- a/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css +++ b/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css @@ -1,3 +1,8 @@ +.root { + -fx-base: #ffffff; + -fx-text-base-color: #000000; +} + /* Sidebar buttons */ .sidebar-btn { -fx-background-color: transparent; @@ -195,7 +200,7 @@ -fx-cursor: hand; } -.global-search { +.global-search-pane { -fx-background-color: #f5f5f5; -fx-background-radius: 15; -fx-text-fill: #0f141a; @@ -293,3 +298,104 @@ -fx-background-radius: 10; -fx-font-weight: bold; } + +/* === Global Search List === */ +.global-search-list .list-cell { + -fx-padding: 8 10; + -fx-background-color: transparent; + -fx-border-color: #ddd; /* light divider */ + -fx-border-width: 0 0 0.5 0; + -fx-alignment: CENTER_LEFT; +} + +.global-search-list .list-cell:hover { + -fx-background-color: rgba(0, 0, 0, 0.05); /* light hover */ +} + +.global-search-title { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: black; +} + +.global-search-subtitle { + -fx-font-size: 12px; + -fx-text-fill: #666; /* gray for light mode */ +} + +/* Avatar placeholder (if no image) */ +.global-search-avatar { + -fx-background-radius: 50%; + -fx-background-color: #bbb; + -fx-min-width: 36; + -fx-min-height: 36; +} + +/* Global search panel */ +.global-search-pane { + -fx-background-color: -fx-base; +} + +.search-header { + -fx-padding: 8 12; + -fx-background-color: transparent; + -fx-border-color: #ddd; + -fx-border-width: 0 0 1 0; +} + +.search-header-label { + -fx-font-size: 13px; + -fx-font-weight: bold; + -fx-text-fill: -fx-text-base-color; +} + +.no-results-label { + -fx-font-size: 12px; + -fx-text-fill: gray; +} + +.close-btn { + -fx-background-color: transparent; + -fx-cursor: hand; + -fx-font-size: 14px; +} + +/* ===== Global Search Result Panel ===== */ +.search-results-container { + -fx-background-color: -fx-base; + -fx-padding: 8; + -fx-spacing: 6; + -fx-border-color: transparent transparent #ddd transparent; + -fx-border-width: 0 0 1 0; +} + +.search-results-title { + -fx-font-weight: bold; + -fx-font-size: 13px; + -fx-text-fill: -fx-text-base-color; +} + +.search-result-item { + -fx-background-color: transparent; + -fx-padding: 6 10; + -fx-spacing: 8; +} + +.search-result-item:hover { + -fx-background-color: rgba(0,0,0,0.05); +} + +/* Avatar always square, not squeezed */ +.search-result-avatar { + -fx-fit-width: 40; + -fx-fit-height: 40; + -fx-preserve-ratio: true; +} + +/* Close button */ +.search-close-btn { + -fx-background-color: transparent; + -fx-text-fill: -fx-text-base-color; + -fx-font-size: 14px; + -fx-cursor: hand; +} diff --git a/src/main/resources/org/to/telegramfinalproject/Fxml/main.fxml b/src/main/resources/org/to/telegramfinalproject/Fxml/main.fxml index d6db77f..62f4177 100644 --- a/src/main/resources/org/to/telegramfinalproject/Fxml/main.fxml +++ b/src/main/resources/org/to/telegramfinalproject/Fxml/main.fxml @@ -69,44 +69,54 @@ - + -