Search system fixed.

This commit is contained in:
Asal Lotfi
2025-08-31 01:01:05 +03:30
parent dff7b82e34
commit 51daae6bd0
4 changed files with 133 additions and 151 deletions
@@ -21,10 +21,6 @@ public class ChatItemController {
@FXML private ImageView profileImageSystem; @FXML private ImageView profileImageSystem;
@FXML private Circle systemCircle; @FXML private Circle systemCircle;
// Default avatar resource path
private static final String DEFAULT_AVATAR =
"/org/to/telegramfinalproject/Avatars/default_avatar.png";
/** /**
* Set chat item data, including a profile image (or default). * Set chat item data, including a profile image (or default).
* *
@@ -431,7 +431,7 @@ public class ChatPageController {
chatStatus.setText(""); chatStatus.setText("");
if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) {
try { try {
userAvatar.setImage(new Image(entry.getImageUrl())); userAvatar.setImage(new Image(entry.getImageUrl() != null ? entry.getImageUrl() : "/org/to/telegramfinalproject/Avatars/default_user_profile.png"));
userAvatar.setClip(new Circle(18, 18, 18)); userAvatar.setClip(new Circle(18, 18, 18));
} catch (Exception ignored) { } catch (Exception ignored) {
} }
@@ -13,6 +13,7 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.shape.Circle;
import javafx.util.Duration; import javafx.util.Duration;
import org.to.telegramfinalproject.Models.ChatEntry; import org.to.telegramfinalproject.Models.ChatEntry;
import org.to.telegramfinalproject.Client.ActionHandler; import org.to.telegramfinalproject.Client.ActionHandler;
@@ -28,9 +29,10 @@ public class MainController {
// === Global Search === // === Global Search ===
@FXML private TextField searchBar; @FXML private TextField searchBar;
@FXML private VBox globalSearchPane; @FXML private VBox globalSearchPane;
@FXML private ListView<SearchResult> globalSearchResults;
@FXML private VBox noResultsBox; @FXML private VBox noResultsBox;
@FXML private ImageView noResultIcon; @FXML private ImageView noResultIcon;
@FXML private ScrollPane globalSearchScroll;
@FXML private VBox globalSearchResultsContainer;
private enum SearchMode { private enum SearchMode {
GLOBAL, GLOBAL,
CHAT CHAT
@@ -83,10 +85,9 @@ public class MainController {
@FXML @FXML
public void initialize() { public void initialize() {
// addSampleChats(); // Populate chat list
populateChatListFromSession(); populateChatListFromSession();
// Register the scene for automatic CSS updates // Register the scene for automatic CSS updates
Platform.runLater(() -> { Platform.runLater(() -> {
ThemeManager.getInstance().registerScene(mainRoot.getScene()); ThemeManager.getInstance().registerScene(mainRoot.getScene());
@@ -95,17 +96,15 @@ public class MainController {
// Listen for theme changes to update icons & labels manually // Listen for theme changes to update icons & labels manually
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> { ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> {
if (newVal) { if (newVal) {
// Dark mode ON
updateIconsForDarkMode(); updateIconsForDarkMode();
updateLabelsForDarkMode(); updateLabelsForDarkMode();
} else { } else {
// Light mode ON
updateIconsForLightMode(); updateIconsForLightMode();
updateLabelsForLightMode(); updateLabelsForLightMode();
} }
}); });
// Smooth scroll feel // Smooth scroll feel for chat list
scrollPane.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()); scrollPane.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm());
scrollPane.setPannable(true); scrollPane.setPannable(true);
scrollPane.setFitToWidth(true); scrollPane.setFitToWidth(true);
@@ -124,25 +123,7 @@ public class MainController {
mainSplitPane.getDividers().get(0).positionProperty().addListener((o, ov, nv) -> clampDivider()); mainSplitPane.getDividers().get(0).positionProperty().addListener((o, ov, nv) -> clampDivider());
} }
// Search bar enter action
// Search in chat field listener
// searchBar.textProperty().addListener((obs, oldV, newV) -> {
// if (!chatSearchPane.isVisible()) return; // only react if in search mode
//
// if (newV.trim().isEmpty()) {
// chatSearchResults.setVisible(false);
// chatSearchResults.setManaged(false);
// } else {
// chatSearchResults.setVisible(true);
// chatSearchResults.setManaged(true);
// chatSearchResults.getItems().setAll(
// "Result 1: " + newV,
// "Result 2: " + newV,
// "Result 3: " + newV
// );
// }
// });
searchBar.setOnAction(e -> { searchBar.setOnAction(e -> {
String keyword = searchBar.getText().trim(); String keyword = searchBar.getText().trim();
if (keyword.isEmpty()) return; if (keyword.isEmpty()) return;
@@ -154,100 +135,28 @@ public class MainController {
} }
}); });
searchBar.setOnAction(e -> performGlobalSearch(searchBar.getText().trim())); // Typing in search bar shows global search panel
searchBar.textProperty().addListener((obs, o, n) -> {
searchBar.textProperty().addListener((obs,o,n)->{ if (n != null && !n.isBlank()) {
if (n!=null && !n.isBlank()) showGlobalSearchPanel(); if (currentSearchMode == SearchMode.GLOBAL) {
}); showGlobalSearchPanel();
} else if (currentSearchMode == SearchMode.CHAT) {
globalSearchResults.setOnMouseClicked(e -> { showSearchPanel();
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 // Smooth scroll feel for global search results
chatSearchResults.setOnMouseClicked(e -> { globalSearchScroll.getStylesheets().add(
int idx = chatSearchResults.getSelectionModel().getSelectedIndex(); getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
if (idx >= 0 && idx < searchBacking.size()) { );
openSearchResult(searchBacking.get(idx)); globalSearchScroll.setPannable(true);
} globalSearchScroll.setFitToWidth(true);
globalSearchScroll.setFitToHeight(false);
globalSearchScroll.getContent().setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003; // match chat list smoothness
globalSearchScroll.setVvalue(globalSearchScroll.getVvalue() - deltaY);
}); });
} }
// Called from ChatPageController when user clicks search button // Called from ChatPageController when user clicks search button
@@ -255,12 +164,28 @@ public class MainController {
scrollPane.setVisible(false); scrollPane.setVisible(false);
scrollPane.setManaged(false); scrollPane.setManaged(false);
currentSearchMode = SearchMode.CHAT;
chatSearchPane.setVisible(true); chatSearchPane.setVisible(true);
chatSearchPane.setManaged(true); chatSearchPane.setManaged(true);
searchBar.requestFocus(); searchBar.requestFocus();
} }
@FXML
public void closeSearchPanel() {
chatSearchPane.setVisible(false);
chatSearchPane.setManaged(false);
currentSearchMode = SearchMode.GLOBAL;
currentChatId = null;
scrollPane.setVisible(true);
scrollPane.setManaged(true);
searchBar.clear();
}
public void showGlobalSearchPanel() { public void showGlobalSearchPanel() {
scrollPane.setVisible(false); scrollPane.setVisible(false);
scrollPane.setManaged(false); scrollPane.setManaged(false);
@@ -273,15 +198,14 @@ public class MainController {
} }
@FXML @FXML
public void closeSearchPanel() { public void closeGlobalSearch() {
chatSearchPane.setVisible(false); globalSearchPane.setVisible(false);
chatSearchPane.setManaged(false); globalSearchPane.setManaged(false);
currentSearchMode = SearchMode.GLOBAL;
currentChatId = null;
scrollPane.setVisible(true); scrollPane.setVisible(true);
scrollPane.setManaged(true); scrollPane.setManaged(true);
searchBar.clear();
} }
// private void addSampleChats() { // private void addSampleChats() {
@@ -387,11 +311,14 @@ public class MainController {
String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview(); String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview();
String timeText = formatChatTime(chat.getLastMessageTime()); String timeText = formatChatTime(chat.getLastMessageTime());
FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml"));
Node chatItem = loader.load();
ChatItemController controller = loader.getController();
cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), "/org/to/telegramfinalproject/Avatars/default_user_profile.png", chat.getType()); // If chat has a profile picture, pass it; otherwise null
String imageUrl = (chat.getImageUrl() != null && !chat.getImageUrl().isEmpty())
? chat.getImageUrl()
: null;
cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), imageUrl, chat.getType());
item.setOnMouseClicked(e -> openChat(chat)); item.setOnMouseClicked(e -> openChat(chat));
chatListContainer.getChildren().add(item); chatListContainer.getChildren().add(item);
@@ -434,7 +361,6 @@ public class MainController {
ChatPageController controller = loader.getController(); ChatPageController controller = loader.getController();
controller.showChat(chat); controller.showChat(chat);
currentSearchMode = SearchMode.CHAT;
currentChatId = UUID.fromString(chat.getId().toString()); currentChatId = UUID.fromString(chat.getId().toString());
chatDisplayArea.getChildren().setAll(chatPage); chatDisplayArea.getChildren().setAll(chatPage);
@@ -669,7 +595,7 @@ public class MainController {
showGlobalSearchPanel(); showGlobalSearchPanel();
// درخواست به سرور (مثل کنسول) // Request to server
org.json.JSONObject req = new org.json.JSONObject(); org.json.JSONObject req = new org.json.JSONObject();
req.put("action", "search"); req.put("action", "search");
req.put("keyword", keyword); req.put("keyword", keyword);
@@ -766,33 +692,61 @@ public class MainController {
// } // }
private void renderSearchResults(List<SearchResult> results) { private void renderSearchResults(List<SearchResult> results) {
globalSearchResults.getItems().clear(); globalSearchResultsContainer.getChildren().clear();
if (results.isEmpty()) { if (results.isEmpty()) {
noResultsBox.setVisible(true); noResultsBox.setVisible(true);
noResultsBox.setManaged(true); noResultsBox.setManaged(true);
globalSearchResults.setVisible(false); globalSearchScroll.setVisible(false);
globalSearchResults.setManaged(false); globalSearchScroll.setManaged(false);
return; return;
} }
noResultsBox.setVisible(false); noResultsBox.setVisible(false);
noResultsBox.setManaged(false); noResultsBox.setManaged(false);
globalSearchResults.setVisible(true); globalSearchScroll.setVisible(true);
globalSearchResults.setManaged(true); globalSearchScroll.setManaged(true);
globalSearchResults.getItems().addAll(results); for (SearchResult r : results) {
HBox container = new HBox(10);
ImageView avatar = new ImageView();
avatar.setFitWidth(40);
avatar.setFitHeight(40);
avatar.setSmooth(true);
avatar.setPreserveRatio(true);
// 🔑 Clip it into a circle
Circle clip = new Circle(20, 20, 20); // x,y = center, radius = 20
avatar.setClip(clip);
Label title = new Label(r.title);
title.getStyleClass().add("global-search-title");
Label subtitle = new Label(
r.type == SRType.MESSAGE
? (r.subtitle != null ? r.subtitle : "")
: "Press to see messages"
);
subtitle.getStyleClass().add("global-search-subtitle");
VBox texts = new VBox(2, title, subtitle);
container.getChildren().addAll(avatar, texts);
// Default profile per type
switch (r.type) {
case USER -> avatar.setImage(new Image(Objects.requireNonNull(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"))));
case GROUP -> avatar.setImage(new Image(Objects.requireNonNull(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_group_profile.png"))));
case CHANNEL -> avatar.setImage(new Image(Objects.requireNonNull(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_channel_profile.png"))));
case MESSAGE -> avatar.setImage(new Image(Objects.requireNonNull(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"))));
} }
@FXML container.setOnMouseClicked(e -> openSearchResult(r));
public void closeGlobalSearch() { globalSearchResultsContainer.getChildren().add(container);
globalSearchPane.setVisible(false); }
globalSearchPane.setManaged(false);
scrollPane.setVisible(true);
scrollPane.setManaged(true);
searchBar.clear();
} }
private void openSearchResult(SearchResult r) { private void openSearchResult(SearchResult r) {
@@ -44,10 +44,14 @@
</graphic> </graphic>
</Button> </Button>
<HBox spacing="5" alignment="CENTER" HBox.hgrow="ALWAYS">
<TextField fx:id="searchBar" <TextField fx:id="searchBar"
prefHeight="36" prefWidth="430.0"
promptText="Search" promptText="Search"
HBox.hgrow="ALWAYS"
maxWidth="Infinity"
styleClass="global-search"/> styleClass="global-search"/>
</HBox>
</children> </children>
</HBox> </HBox>
@@ -104,8 +108,36 @@
<Button text="✕" onAction="#closeGlobalSearch" styleClass="search-close-btn"/> <Button text="✕" onAction="#closeGlobalSearch" styleClass="search-close-btn"/>
</HBox> </HBox>
<!-- Results --> <!-- Search results -->
<ListView fx:id="globalSearchResults" styleClass="global-search-list"/> <ScrollPane fx:id="globalSearchScroll"
fitToWidth="true"
hbarPolicy="NEVER"
vbarPolicy="AS_NEEDED"
styleClass="chat-scroll">
<content>
<VBox fx:id="globalSearchResultsContainer" spacing="5"
styleClass="global-search-list">
<!-- One search result item -->
<HBox spacing="10" alignment="CENTER_LEFT" styleClass="search-result-item">
<!-- Avatar -->
<StackPane>
<ImageView fx:id="searchResultAvatar"
fitWidth="40" fitHeight="40"
preserveRatio="true"
smooth="true"
cache="true"
styleClass="avatar"/>
</StackPane>
<!-- Texts -->
<VBox spacing="2">
<Label fx:id="searchResultTitle" styleClass="global-search-title"/>
<Label fx:id="searchResultSubtitle" styleClass="global-search-subtitle"/>
</VBox>
</HBox>
</VBox>
</content>
</ScrollPane>
<!-- No Results Placeholder --> <!-- No Results Placeholder -->
<VBox fx:id="noResultsBox" alignment="CENTER" spacing="10" visible="false" managed="false"> <VBox fx:id="noResultsBox" alignment="CENTER" spacing="10" visible="false" managed="false">