Group info UI + backend implemented.
@@ -679,4 +679,83 @@ public class GroupDatabase {
|
|||||||
// return result;
|
// return result;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
public static JSONObject getGroupInfo(UUID groupId, UUID viewerUuid) throws SQLException {
|
||||||
|
JSONObject result = new JSONObject();
|
||||||
|
|
||||||
|
try (Connection conn = ConnectionDb.connect()) {
|
||||||
|
// === Group header (name, member count, etc.)
|
||||||
|
String groupQuery = """
|
||||||
|
SELECT g.internal_uuid, g.group_id, g.group_name, g.image_url,
|
||||||
|
COUNT(m.user_id) as member_count
|
||||||
|
FROM groups g
|
||||||
|
LEFT JOIN group_members m ON g.internal_uuid = m.group_id
|
||||||
|
WHERE g.internal_uuid = ?
|
||||||
|
GROUP BY g.internal_uuid, g.group_id, g.group_name, g.image_url
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (PreparedStatement ps = conn.prepareStatement(groupQuery)) {
|
||||||
|
ps.setObject(1, groupId); // ✅ compare UUID with UUID
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
if (rs.next()) {
|
||||||
|
result.put("internal_uuid", rs.getString("internal_uuid"));
|
||||||
|
result.put("group_id", rs.getString("group_id")); // varchar handle
|
||||||
|
result.put("group_name", rs.getString("group_name"));
|
||||||
|
result.put("member_count", rs.getInt("member_count"));
|
||||||
|
result.put("image_url", rs.getString("image_url"));
|
||||||
|
} else {
|
||||||
|
return null; // no such group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Members (id, name, role, status, image)
|
||||||
|
JSONArray membersArr = new JSONArray();
|
||||||
|
|
||||||
|
String membersQuery = """
|
||||||
|
SELECT u.internal_uuid, u.profile_name, u.user_id, u.image_url,
|
||||||
|
gm.role, u.status, u.last_seen
|
||||||
|
FROM group_members gm
|
||||||
|
JOIN users u ON gm.user_id = u.internal_uuid
|
||||||
|
WHERE gm.group_id = ?
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (PreparedStatement ps = conn.prepareStatement(membersQuery)) {
|
||||||
|
ps.setObject(1, groupId); // ✅ gm.group_id is UUID
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
while (rs.next()) {
|
||||||
|
JSONObject member = new JSONObject();
|
||||||
|
member.put("user_id", rs.getString("internal_uuid"));
|
||||||
|
member.put("profile_name", rs.getString("profile_name"));
|
||||||
|
member.put("username", rs.getString("user_id"));
|
||||||
|
member.put("image_url", rs.getString("image_url"));
|
||||||
|
member.put("role", rs.getString("role"));
|
||||||
|
member.put("status", rs.getString("status"));
|
||||||
|
member.put("last_seen", rs.getString("last_seen"));
|
||||||
|
membersArr.put(member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.put("members", membersArr);
|
||||||
|
|
||||||
|
// === Viewer role (to decide if they can delete group, etc.)
|
||||||
|
String roleQuery = """
|
||||||
|
SELECT role
|
||||||
|
FROM group_members
|
||||||
|
WHERE group_id = ? AND user_id = ?
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (PreparedStatement ps = conn.prepareStatement(roleQuery)) {
|
||||||
|
ps.setObject(1, groupId);
|
||||||
|
ps.setObject(2, viewerUuid);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
if (rs.next()) {
|
||||||
|
result.put("my_role", rs.getString("role"));
|
||||||
|
} else {
|
||||||
|
result.put("my_role", ""); // not a member
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2997,7 +2997,29 @@ public class ClientHandler implements Runnable {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "view_group": {
|
||||||
|
if (currentUser == null) {
|
||||||
|
response = new ResponseModel("error", "Unauthorized. Please login first.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
UUID groupId = UUID.fromString(requestJson.getString("group_id"));
|
||||||
|
UUID viewerId = UUID.fromString(requestJson.getString("viewer_id"));
|
||||||
|
|
||||||
|
// Query group details
|
||||||
|
JSONObject groupData = GroupDatabase.getGroupInfo(groupId, viewerId);
|
||||||
|
|
||||||
|
if (groupData == null) {
|
||||||
|
response = new ResponseModel("error", "Group not found.");
|
||||||
|
} else {
|
||||||
|
response = new ResponseModel("success", "Group info fetched.", groupData);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
response = new ResponseModel("error", "Error processing group info: " + e.getMessage());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public class BlockedUsersController {
|
|||||||
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> updateBackIcon(newVal));
|
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> updateBackIcon(newVal));
|
||||||
updateBackIcon(ThemeManager.getInstance().isDarkMode());
|
updateBackIcon(ThemeManager.getInstance().isDarkMode());
|
||||||
|
|
||||||
// اسکرول نرم
|
// Smooth scroll feel
|
||||||
blockedList.getStylesheets().add(
|
blockedList.getStylesheets().add(
|
||||||
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
|
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -954,28 +954,38 @@ public class ChatPageController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void configureHeaderActions(ChatEntry entry) {
|
private void configureHeaderActions(ChatEntry entry) {
|
||||||
chatMoreMenu.getItems().clear();
|
// Hide everything by default
|
||||||
|
archiveItem.setVisible(false);
|
||||||
|
viewProfileItem.setVisible(false);
|
||||||
|
deleteChatItem.setVisible(false);
|
||||||
|
viewGroupItem.setVisible(false);
|
||||||
|
leaveGroupItem.setVisible(false);
|
||||||
|
viewChannelItem.setVisible(false);
|
||||||
|
leaveChannelItem.setVisible(false);
|
||||||
|
|
||||||
switch (entry.getType().toLowerCase(Locale.ROOT)) {
|
switch (entry.getType().toLowerCase(Locale.ROOT)) {
|
||||||
case "private" -> {
|
case "private" -> {
|
||||||
|
archiveItem.setVisible(true);
|
||||||
|
viewProfileItem.setVisible(true);
|
||||||
|
deleteChatItem.setVisible(true);
|
||||||
|
|
||||||
archiveItem.setOnAction(e -> toggleArchive(entry));
|
archiveItem.setOnAction(e -> toggleArchive(entry));
|
||||||
viewProfileItem.setOnAction(e -> openInfoScene(entry));
|
viewProfileItem.setOnAction(e -> openInfoScene(entry));
|
||||||
deleteChatItem.setOnAction(e -> deleteChatButton(entry));
|
deleteChatItem.setOnAction(e -> deleteChatButton(entry));
|
||||||
chatMoreMenu.getItems().addAll(archiveItem, viewProfileItem, deleteChatItem);
|
|
||||||
}
|
}
|
||||||
case "group" -> {
|
case "group" -> {
|
||||||
MenuItem viewGroup = new MenuItem("View group info");
|
viewGroupItem.setVisible(true);
|
||||||
viewGroup.setOnAction(e -> openInfoScene(entry));
|
leaveGroupItem.setVisible(true);
|
||||||
MenuItem leaveGroup = new MenuItem("Leave group");
|
|
||||||
leaveGroup.setOnAction(e -> leaveGroupButton(entry));
|
viewGroupItem.setOnAction(e -> openInfoScene(entry));
|
||||||
chatMoreMenu.getItems().addAll(viewGroup, leaveGroup);
|
leaveGroupItem.setOnAction(e -> leaveGroupButton(entry));
|
||||||
}
|
}
|
||||||
case "channel" -> {
|
case "channel" -> {
|
||||||
MenuItem viewChannel = new MenuItem("View channel info");
|
viewChannelItem.setVisible(true);
|
||||||
viewChannel.setOnAction(e -> openInfoScene(entry));
|
leaveChannelItem.setVisible(true);
|
||||||
MenuItem leaveChannel = new MenuItem("Leave channel");
|
|
||||||
leaveChannel.setOnAction(e -> leaveChannelButton(entry));
|
viewChannelItem.setOnAction(e -> openInfoScene(entry));
|
||||||
chatMoreMenu.getItems().addAll(viewChannel, leaveChannel);
|
leaveChannelItem.setOnAction(e -> leaveChannelButton(entry));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1027,7 +1037,8 @@ public class ChatPageController {
|
|||||||
}
|
}
|
||||||
case "group" -> {
|
case "group" -> {
|
||||||
req.put("action", "view_group")
|
req.put("action", "view_group")
|
||||||
.put("group_id", entry.getId().toString());
|
.put("group_id", entry.getId().toString())
|
||||||
|
.put("viewer_id", Session.getUserUUID());
|
||||||
}
|
}
|
||||||
case "channel" -> {
|
case "channel" -> {
|
||||||
req.put("action", "view_channel")
|
req.put("action", "view_channel")
|
||||||
@@ -3036,6 +3047,14 @@ public class ChatPageController {
|
|||||||
if (deleteChatItem != null && deleteChatItem.getGraphic() instanceof ImageView iv) {
|
if (deleteChatItem != null && deleteChatItem.getGraphic() instanceof ImageView iv) {
|
||||||
iv.setImage(loadIcon("delete_red.png")); // stays red in both themes
|
iv.setImage(loadIcon("delete_red.png")); // stays red in both themes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (viewGroupItem != null && viewGroupItem.getGraphic() instanceof ImageView iv) {
|
||||||
|
iv.setImage(loadIcon("group" + suffix));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewChannelItem != null && viewChannelItem.getGraphic() instanceof ImageView iv) {
|
||||||
|
iv.setImage(loadIcon("group" + suffix));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChatPageController
|
// ChatPageController
|
||||||
|
|||||||
@@ -1,11 +1,249 @@
|
|||||||
package org.to.telegramfinalproject.UI;
|
package org.to.telegramfinalproject.UI;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
import org.to.telegramfinalproject.Client.ActionHandler;
|
||||||
|
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
|
||||||
import org.to.telegramfinalproject.Models.ChatEntry;
|
import org.to.telegramfinalproject.Models.ChatEntry;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
public class GroupInfoController {
|
public class GroupInfoController {
|
||||||
|
|
||||||
|
@FXML private VBox groupCard;
|
||||||
|
@FXML private Pane overlayBackground;
|
||||||
|
@FXML private Button closeButton;
|
||||||
|
|
||||||
|
@FXML private ImageView groupImage;
|
||||||
|
@FXML private Label groupName;
|
||||||
|
@FXML private Label memberCount;
|
||||||
|
|
||||||
|
@FXML private Button infoMoreButton;
|
||||||
|
@FXML private ContextMenu infoMoreMenu;
|
||||||
|
@FXML private MenuItem addMemberItem;
|
||||||
|
@FXML private MenuItem manageGroupItem;
|
||||||
|
@FXML private MenuItem deleteGroupItem;
|
||||||
|
@FXML private ImageView moreIcon;
|
||||||
|
|
||||||
|
@FXML private VBox membersList;
|
||||||
|
@FXML private Label membersHeader;
|
||||||
|
@FXML private Button addMemberButton;
|
||||||
|
@FXML private ImageView membersIcon;
|
||||||
|
@FXML private ScrollPane membersScroll;
|
||||||
|
|
||||||
|
private String groupId; // the UUID of this group
|
||||||
|
|
||||||
|
private static final String ICON_PATH = "/org/to/telegramfinalproject/Icons/";
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void initialize() {
|
||||||
|
closeButton.setOnAction(e ->
|
||||||
|
MainController.getInstance().closeOverlay(groupCard.getParent()));
|
||||||
|
overlayBackground.setOnMouseClicked(e ->
|
||||||
|
MainController.getInstance().closeOverlay(groupCard.getParent()));
|
||||||
|
|
||||||
|
infoMoreButton.setOnAction(e -> {
|
||||||
|
if (infoMoreMenu != null) infoMoreMenu.show(infoMoreButton, javafx.geometry.Side.BOTTOM, 0, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteGroupItem.setOnAction(e -> handleDeleteGroup());
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if (groupCard.getScene() != null) {
|
||||||
|
ThemeManager.getInstance().registerScene(groupCard.getScene());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> {
|
||||||
|
updateIcons(newVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateIcons(ThemeManager.getInstance().isDarkMode());
|
||||||
|
|
||||||
|
// Smooth scroll feel
|
||||||
|
membersScroll.getStylesheets().add(
|
||||||
|
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
|
||||||
|
);
|
||||||
|
membersScroll.skinProperty().addListener((obs, oldSkin, newSkin) -> {
|
||||||
|
if (newSkin != null) {
|
||||||
|
ScrollBar vBar = (ScrollBar) membersScroll.lookup(".scroll-bar:vertical");
|
||||||
|
if (vBar != null) {
|
||||||
|
membersScroll.setOnScroll(event -> {
|
||||||
|
double deltaY = event.getDeltaY() * 0.003;
|
||||||
|
double newValue = vBar.getValue() - deltaY;
|
||||||
|
vBar.setValue(Math.max(0, Math.min(newValue, 1)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (addMemberButton != null) {
|
||||||
|
addMemberButton.setOnAction(e -> openAddMemberScene());
|
||||||
|
}
|
||||||
|
if (addMemberItem != null) {
|
||||||
|
addMemberItem.setOnAction(e -> openAddMemberScene());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setGroupDataFromJson(ChatEntry entry, JSONObject data) {
|
public void setGroupDataFromJson(ChatEntry entry, JSONObject data) {
|
||||||
// read name, status, bio, image url from JSON
|
this.groupId = data.optString("internal_uuid", entry.getId().toString());
|
||||||
// update labels/images accordingly
|
|
||||||
|
groupName.setText(data.optString("group_name", entry.getName()));
|
||||||
|
memberCount.setText(data.optInt("member_count", 0) + " members");
|
||||||
|
|
||||||
|
// show/hide Delete button if user is owner
|
||||||
|
String myRole = data.optString("my_role", "member");
|
||||||
|
if ("owner".equalsIgnoreCase(myRole)) {
|
||||||
|
deleteGroupItem.setVisible(true);
|
||||||
|
} else {
|
||||||
|
deleteGroupItem.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group picture
|
||||||
|
String imgUrl = data.optString("image_url", "");
|
||||||
|
if (!imgUrl.isBlank()) {
|
||||||
|
try {
|
||||||
|
Image img = AvatarLocalResolver.load(imgUrl);
|
||||||
|
if (img != null) groupImage.setImage(img);
|
||||||
|
} catch (Exception ignore) {}
|
||||||
|
} else {
|
||||||
|
groupImage.setImage(
|
||||||
|
new Image(getClass().getResourceAsStream(
|
||||||
|
"/org/to/telegramfinalproject/Avatars/default_group_profile.png"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Members
|
||||||
|
membersList.getChildren().clear();
|
||||||
|
var arr = data.optJSONArray("members");
|
||||||
|
if (arr != null) {
|
||||||
|
membersHeader.setText(arr.length() + " MEMBERS");
|
||||||
|
for (int i = 0; i < arr.length(); i++) {
|
||||||
|
JSONObject m = arr.getJSONObject(i);
|
||||||
|
addMemberRow(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addMemberRow(JSONObject m) {
|
||||||
|
HBox row = new HBox(10);
|
||||||
|
row.getStyleClass().add("member-row");
|
||||||
|
row.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
// === Avatar ===
|
||||||
|
ImageView avatar = new ImageView();
|
||||||
|
avatar.setFitWidth(36);
|
||||||
|
avatar.setFitHeight(36);
|
||||||
|
avatar.setPreserveRatio(true);
|
||||||
|
AvatarFX.circleClip(avatar, 36);
|
||||||
|
|
||||||
|
String imgUrl = m.optString("image_url", "");
|
||||||
|
if (!imgUrl.isBlank()) {
|
||||||
|
Image img = AvatarLocalResolver.load(imgUrl);
|
||||||
|
if (img != null) avatar.setImage(img);
|
||||||
|
} else {
|
||||||
|
avatar.setImage(new Image(getClass().getResourceAsStream(
|
||||||
|
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Name + Status ===
|
||||||
|
VBox nameBox = new VBox(2);
|
||||||
|
Label name = new Label(m.optString("profile_name", "Unknown"));
|
||||||
|
name.getStyleClass().add("member-name");
|
||||||
|
|
||||||
|
// Status = online / last seen recently
|
||||||
|
String status;
|
||||||
|
if (m.optBoolean("is_online", false)) {
|
||||||
|
status = "online";
|
||||||
|
} else {
|
||||||
|
status = ChatPageController.getInstance().userStatusText(
|
||||||
|
false,
|
||||||
|
m.optString("last_seen", null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Label statusLbl = new Label(status);
|
||||||
|
statusLbl.getStyleClass().add("member-status");
|
||||||
|
|
||||||
|
nameBox.getChildren().addAll(name, statusLbl);
|
||||||
|
|
||||||
|
// === Role (owner/admin/member) ===
|
||||||
|
Label role = new Label();
|
||||||
|
String roleStr = m.optString("role", "");
|
||||||
|
if (!roleStr.isBlank()) {
|
||||||
|
role.setText(roleStr.toLowerCase());
|
||||||
|
role.getStyleClass().add("member-role");
|
||||||
|
}
|
||||||
|
|
||||||
|
Region spacer = new Region();
|
||||||
|
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||||
|
|
||||||
|
row.getChildren().addAll(avatar, nameBox, spacer, role);
|
||||||
|
membersList.getChildren().add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openAddMemberScene() {
|
||||||
|
try {
|
||||||
|
FXMLLoader loader = new FXMLLoader(getClass().getResource(
|
||||||
|
"/org/to/telegramfinalproject/Fxml/add_member.fxml"));
|
||||||
|
Node overlay = loader.load();
|
||||||
|
|
||||||
|
AddMembersController controller = loader.getController();
|
||||||
|
// 👇 Pass group id so AddMember scene knows which group to add to
|
||||||
|
controller.setGroupForAdd(groupId, groupName);
|
||||||
|
|
||||||
|
MainController.getInstance().showOverlay(overlay);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
MainController.getInstance().showAlert(
|
||||||
|
"Error",
|
||||||
|
"Could not load Add Member scene.",
|
||||||
|
Alert.AlertType.ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleDeleteGroup() {
|
||||||
|
System.out.println("Deleting group...");
|
||||||
|
// TODO: implement backend call
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateIcons(boolean dark) {
|
||||||
|
String suffix = dark ? "_light.png" : "_dark.png";
|
||||||
|
|
||||||
|
moreIcon.setImage(loadImage(ICON_PATH + "more" + suffix));
|
||||||
|
membersIcon.setImage(loadImage(ICON_PATH + "group" + suffix));
|
||||||
|
addMemberButton.setGraphic(makeIcon(ICON_PATH + "add_member" + suffix));
|
||||||
|
membersIcon.setImage(loadImage(ICON_PATH + "group_member" + suffix));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- helpers -------------------------------------------------------------
|
||||||
|
|
||||||
|
private ImageView makeIcon(String path) {
|
||||||
|
ImageView iv = new ImageView();
|
||||||
|
Image img = loadImage(path);
|
||||||
|
if (img != null) {
|
||||||
|
iv.setImage(img);
|
||||||
|
iv.setFitWidth(22);
|
||||||
|
iv.setFitHeight(22);
|
||||||
|
iv.setPreserveRatio(true);
|
||||||
|
}
|
||||||
|
return iv;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Image loadImage(String path) {
|
||||||
|
URL res = getClass().getResource(path);
|
||||||
|
if (res == null) return null;
|
||||||
|
return new Image(res.toExternalForm());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ public class NewChannelController {
|
|||||||
|
|
||||||
createButton.setOnAction(e -> onCreateChannel());
|
createButton.setOnAction(e -> onCreateChannel());
|
||||||
|
|
||||||
// محدودیت توضیح
|
// Limit description
|
||||||
final int MAX_LENGTH = 255;
|
final int MAX_LENGTH = 255;
|
||||||
channelDescField.addEventFilter(javafx.scene.input.KeyEvent.KEY_TYPED, e -> {
|
channelDescField.addEventFilter(javafx.scene.input.KeyEvent.KEY_TYPED, e -> {
|
||||||
if (channelDescField.getText().length() >= MAX_LENGTH) e.consume();
|
if (channelDescField.getText().length() >= MAX_LENGTH) e.consume();
|
||||||
@@ -79,7 +79,7 @@ public class NewChannelController {
|
|||||||
});
|
});
|
||||||
descCounter.setText("0 / 255");
|
descCounter.setText("0 / 255");
|
||||||
|
|
||||||
// پاک کردن استایل خطا هنگام تایپ
|
// Reset error when user types again
|
||||||
channelNameField.textProperty().addListener((obs, ov, nv) -> {
|
channelNameField.textProperty().addListener((obs, ov, nv) -> {
|
||||||
if (!nv.trim().isEmpty()) {
|
if (!nv.trim().isEmpty()) {
|
||||||
channelNameField.getStyleClass().remove("error");
|
channelNameField.getStyleClass().remove("error");
|
||||||
|
|||||||
@@ -1132,3 +1132,59 @@
|
|||||||
-fx-border-radius: 50%;
|
-fx-border-radius: 50%;
|
||||||
-fx-border-width: 1;
|
-fx-border-width: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.member-row {
|
||||||
|
-fx-alignment: CENTER_LEFT;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-scroll {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-card {
|
||||||
|
-fx-background-radius: 12;
|
||||||
|
-fx-background-color: #1f2a36; /* dark gray card */
|
||||||
|
-fx-padding: 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-title {
|
||||||
|
-fx-font-size: 16px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-text-fill: #e8f1f8; /* light text */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.member-row {
|
||||||
|
-fx-padding: 6 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-name {
|
||||||
|
-fx-font-size: 14px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-text-fill: #e8f1f8; /* Telegram light text */
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-status {
|
||||||
|
-fx-font-size: 12px;
|
||||||
|
-fx-text-fill: #8ea1b2; /* grayish */
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-role {
|
||||||
|
-fx-font-size: 12px;
|
||||||
|
-fx-text-fill: #4fa8f0; /* blue */
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Group name (bold, light text) */
|
||||||
|
.info-title {
|
||||||
|
-fx-font-size: 16px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-text-fill: #e8f1f8; /* light gray/white */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Members count (blue link style like Telegram dark mode) */
|
||||||
|
.info-subtitle {
|
||||||
|
-fx-font-size: 13px;
|
||||||
|
-fx-text-fill: #4fa8f0; /* same Telegram blue */
|
||||||
|
}
|
||||||
|
|||||||
@@ -1123,3 +1123,58 @@
|
|||||||
-fx-border-radius: 50%;
|
-fx-border-radius: 50%;
|
||||||
-fx-border-width: 1;
|
-fx-border-width: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.member-row {
|
||||||
|
-fx-alignment: CENTER_LEFT;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-scroll {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-card {
|
||||||
|
-fx-background-radius: 12;
|
||||||
|
-fx-background-color: #ffffff; /* pure white card */
|
||||||
|
-fx-padding: 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-title {
|
||||||
|
-fx-font-size: 16px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-text-fill: #0f141a; /* dark text */
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-row {
|
||||||
|
-fx-padding: 6 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-name {
|
||||||
|
-fx-font-size: 14px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-text-fill: #0f141a; /* Telegram dark gray for names */
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-status {
|
||||||
|
-fx-font-size: 12px;
|
||||||
|
-fx-text-fill: #8ea1b2; /* grayish */
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-role {
|
||||||
|
-fx-font-size: 12px;
|
||||||
|
-fx-text-fill: #4fa8f0; /* blue */
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Group name (bold, dark text) */
|
||||||
|
.info-title {
|
||||||
|
-fx-font-size: 16px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-text-fill: #0f141a; /* deep gray */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Members count (blue link style like Telegram) */
|
||||||
|
.info-subtitle {
|
||||||
|
-fx-font-size: 13px;
|
||||||
|
-fx-text-fill: #4fa8f0; /* Telegram blue */
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,28 +50,60 @@
|
|||||||
<contextMenu>
|
<contextMenu>
|
||||||
<ContextMenu fx:id="chatMoreMenu">
|
<ContextMenu fx:id="chatMoreMenu">
|
||||||
<items>
|
<items>
|
||||||
<MenuItem fx:id="archiveItem" text="Archive chat">
|
<!-- Private -->
|
||||||
|
<MenuItem fx:id="archiveItem" text="Archive chat" visible="false">
|
||||||
<graphic>
|
<graphic>
|
||||||
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
<image><Image url="@/org/to/telegramfinalproject/Icons/archived_chats_dark.png"/>
|
<image><Image url="@/org/to/telegramfinalproject/Icons/archived_chats_dark.png"/></image>
|
||||||
</image>
|
|
||||||
</ImageView>
|
</ImageView>
|
||||||
</graphic>
|
</graphic>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem fx:id="viewProfileItem" text="View profile">
|
<MenuItem fx:id="viewProfileItem" text="View profile" visible="false">
|
||||||
<graphic>
|
<graphic>
|
||||||
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
<image><Image url="@/org/to/telegramfinalproject/Icons/view_profile_dark.png"/></image>
|
<image><Image url="@/org/to/telegramfinalproject/Icons/view_profile_dark.png"/></image>
|
||||||
</ImageView>
|
</ImageView>
|
||||||
</graphic>
|
</graphic>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem fx:id="deleteChatItem" text="Delete chat" style="-fx-text-fill: red;">
|
<MenuItem fx:id="deleteChatItem" text="Delete chat" style="-fx-text-fill: red;" visible="false">
|
||||||
<graphic>
|
<graphic>
|
||||||
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
<image><Image url="@/org/to/telegramfinalproject/Icons/delete_red.png"/></image>
|
<image><Image url="@/org/to/telegramfinalproject/Icons/delete_red.png"/></image>
|
||||||
</ImageView>
|
</ImageView>
|
||||||
</graphic>
|
</graphic>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
|
<!-- Group -->
|
||||||
|
<MenuItem fx:id="viewGroupItem" text="View group info" visible="false">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
|
<image><Image url="@/org/to/telegramfinalproject/Icons/group_dark.png"/></image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem fx:id="leaveGroupItem" text="Leave group" style="-fx-text-fill: red;" visible="false">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
|
<image><Image url="@/org/to/telegramfinalproject/Icons/log_out.png"/></image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<!-- Channel -->
|
||||||
|
<MenuItem fx:id="viewChannelItem" text="View channel info" visible="false">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
|
<image><Image url="@/org/to/telegramfinalproject/Icons/group_dark.png"/></image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem fx:id="leaveChannelItem" text="Leave channel" style="-fx-text-fill: red;" visible="false">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
|
<image><Image url="@/org/to/telegramfinalproject/Icons/log_out.png"/></image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</MenuItem>
|
||||||
</items>
|
</items>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</contextMenu>
|
</contextMenu>
|
||||||
|
|||||||
@@ -1,14 +1,119 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
<?import java.lang.*?>
|
|
||||||
<?import java.util.*?>
|
|
||||||
<?import javafx.scene.*?>
|
|
||||||
<?import javafx.scene.control.*?>
|
|
||||||
<?import javafx.scene.layout.*?>
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
|
||||||
<AnchorPane xmlns="http://javafx.com/javafx"
|
<?import javafx.scene.image.Image?>
|
||||||
xmlns:fx="http://javafx.com/fxml"
|
<StackPane xmlns:fx="http://javafx.com/fxml"
|
||||||
fx:controller="org.to.telegramfinalproject.UI.GroupInfoController"
|
fx:controller="org.to.telegramfinalproject.UI.GroupInfoController"
|
||||||
prefHeight="400.0" prefWidth="600.0">
|
styleClass="overlay-root">
|
||||||
|
|
||||||
</AnchorPane>
|
<!-- Background -->
|
||||||
|
<Pane fx:id="overlayBackground" styleClass="overlay-background"/>
|
||||||
|
|
||||||
|
<!-- Group card -->
|
||||||
|
<VBox fx:id="groupCard" styleClass="profile-card" spacing="16"
|
||||||
|
prefWidth="360" maxWidth="360"
|
||||||
|
prefHeight="500" maxHeight="500">
|
||||||
|
|
||||||
|
<!-- ===== Header: Title + More + Close ===== -->
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="10">
|
||||||
|
<Label text="Group Info" styleClass="profile-title" HBox.hgrow="ALWAYS"/>
|
||||||
|
<Pane HBox.hgrow="ALWAYS"/>
|
||||||
|
|
||||||
|
<!-- More -->
|
||||||
|
<Button fx:id="infoMoreButton" styleClass="icon-button">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fx:id="moreIcon" fitWidth="18" fitHeight="18" preserveRatio="true"/>
|
||||||
|
</graphic>
|
||||||
|
<contextMenu>
|
||||||
|
<ContextMenu fx:id="infoMoreMenu">
|
||||||
|
<items>
|
||||||
|
<!-- Add Member -->
|
||||||
|
<MenuItem fx:id="addMemberItem" text="Add member">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
|
<image>
|
||||||
|
<Image url="@/org/to/telegramfinalproject/Icons/add_member_dark.png"/>
|
||||||
|
</image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<!-- Manage Group -->
|
||||||
|
<MenuItem fx:id="manageGroupItem" text="Manage group">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
|
<image>
|
||||||
|
<Image url="@/org/to/telegramfinalproject/Icons/manage_dark.png"/>
|
||||||
|
</image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<!-- Delete Chat (owner only) -->
|
||||||
|
<MenuItem fx:id="deleteGroupItem" text="Delete chat" style="-fx-text-fill: red;" visible="false">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
|
<image>
|
||||||
|
<Image url="@/org/to/telegramfinalproject/Icons/delete_red.png"/>
|
||||||
|
</image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</MenuItem>
|
||||||
|
</items>
|
||||||
|
</ContextMenu>
|
||||||
|
</contextMenu>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Close -->
|
||||||
|
<Button fx:id="closeButton" styleClass="icon-button" text="✕"/>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<!-- ===== Group picture + name + members count ===== -->
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="12">
|
||||||
|
<!-- Group picture -->
|
||||||
|
<ImageView fx:id="groupImage" fitWidth="60" fitHeight="60" preserveRatio="true"
|
||||||
|
styleClass="profile-picture"/>
|
||||||
|
|
||||||
|
<!-- Name + members stacked -->
|
||||||
|
<VBox alignment="CENTER_LEFT" spacing="4">
|
||||||
|
<Label fx:id="groupName" text="Group name" styleClass="info-title"/>
|
||||||
|
<Label fx:id="memberCount" text="0 members" styleClass="info-subtitle"/>
|
||||||
|
</VBox>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<Separator styleClass="section-separator"/>
|
||||||
|
|
||||||
|
<!-- ===== Members list ===== -->
|
||||||
|
<VBox spacing="8" styleClass="info-blocks" VBox.vgrow="ALWAYS">
|
||||||
|
<HBox spacing="8" alignment="CENTER_LEFT">
|
||||||
|
<ImageView fx:id="membersIcon" fitWidth="26" fitHeight="26" preserveRatio="true">
|
||||||
|
<image>
|
||||||
|
<Image url="@/org/to/telegramfinalproject/Icons/group_member_dark.png"/>
|
||||||
|
</image>
|
||||||
|
</ImageView>
|
||||||
|
<Label fx:id="membersHeader" text="0 MEMBERS" styleClass="profile-info-sub"/>
|
||||||
|
<Pane HBox.hgrow="ALWAYS"/>
|
||||||
|
<Button fx:id="addMemberButton" styleClass="icon-button">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
|
<image>
|
||||||
|
<Image url="@/org/to/telegramfinalproject/Icons/add_member_dark.png"/>
|
||||||
|
</image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</Button>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<ScrollPane fx:id="membersScroll" fitToWidth="true" styleClass="member-scroll" VBox.vgrow="ALWAYS">
|
||||||
|
<content>
|
||||||
|
<VBox fx:id="membersList" spacing="10"/>
|
||||||
|
</content>
|
||||||
|
</ScrollPane>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
</VBox>
|
||||||
|
</StackPane>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 899 B |
|
After Width: | Height: | Size: 920 B |
|
After Width: | Height: | Size: 915 B |
|
After Width: | Height: | Size: 339 B |
|
After Width: | Height: | Size: 801 B |
|
After Width: | Height: | Size: 684 B |